Source code for armi.reactor.parameters.resolveCollections
# Copyright 2019 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the magic that makes the Parameter system and ARMI composite model
play nicely together.
The contained metaclass is useful for maintaining a hierarchy of ``ParameterCollection``
classes, which mimic the hierarchy of ``ArmiObject`` s to which they apply. Some
``ArmiObject`` subclasses define their own parameters, while others do not, so we do not
want to blindly create a ``ParameterCollectionClass`` for each ``ArmiObject`` subclass.
Instead, we want to be able to skip generations when no additional parameters were
requested for that level. For instance if we have a hierarchy like: ``ArmiObject`` <-
``A`` <- ``B``, where ``ArmiObject`` and ``B`` define parameters, while ``A`` does not
define any parameters of its own, we want to have a ``ArmiObjectParameterCollection``
and a ``BParameterCollection`` (with ``BParameterCollection`` being a subclass of
``ArmiObjectParameterCollection``). ``ArmiObject`` and ``A`` will both *share* the
``ArmiObjectParameterCollection``, while ``B`` will use ``BParameterCollection``.
``BParameterCollection`` will contain all of the parameters defined in
``ArmiObjectParameterCollection``, plus whatever additional parameters were defined on
``B``.
The above scenario should behave rather intuitively for someone used to classes and
inheritance, but maintaining this hierarchy by hand would be onerous and error-prone.
What if one day we decide to add some parameters to ``A``? We need to remember to add a
new class for its parameters, and make sure to make ``BParameterCollection`` a subclass
of that new ``ParameterCollection`` class. With the below metaclass, we needn't worry
ourselves with any of that; it is taken care of automatically.
If you want to know how the sausage is made, the ``ResolveParametersMeta`` metaclass is
responsible for forming a hierarchy of ``ParameterCollection`` classes that correspond
to the related hierarchy of classes inheriting the root ``ArmiObject`` class. It should
be rare for an ARMI developer not engaged directly with Framework development to need to
know exactly how this works, but a proficient ARMI developer must keep in mind the
following rules about how this system behaves in practice:
* When defining subclasses of ``ArmiObject``, defining a class attribute called
``pDefs`` of the ``ParameterDefinitionCollection`` type signals to the system that
this is a *Parameter Class*.
* When defining a *Parameter Class*, it will trigger the creation of a new
``ParameterCollection`` class, which will be derived from the
``ParameterCollection`` class of the most immediate *Parameter Class* ancestor
the new class's inheritance tree.
* All classes derived from ``ArmiObject`` will receive an associated subclass of
``ParameterCollection``, which will ultimately include all of the relevant
Parameters for that class. The specific class is the ``ParameterCollection``
subclass defined for the most immediate *Parameter Class* in the classes
inheritance tree.
* Parameter definitions can be added to a *Parameter Class*'s ``pDefs`` until
Parameters have been "compiled" for it. After compiling parameters, the ``pDefs``
are locked, and any attempts at defining additional parameters will cause an
error.
* ``ArmiObject`` s cannot be instantiated until after parameters have been compiled.
"""
from armi.reactor.parameters.parameterCollections import ParameterCollection
from armi.reactor.parameters.parameterDefinitions import ParameterDefinitionCollection
[docs]class ResolveParametersMeta(type):
"""Metaclass for automatically defining associated ParameterCollection classes.
Any class invoking this metaclass will automatically create an associated sub-class
of the ``ParameterCollection`` type, if it has a class attribute called ``pDefs``
that is an instance of ``ParameterDefinitionCollection``. This new class will itself
be a subclass of the ``ParameterCollection`` class that is associated with the
invoking class's parent.
If no ``pDefs`` class attribute is present, the invoking class will adopt the
``ParameterCollection`` class associated with it's parent, or ``None`` if it cannot
find one.
The associated ``ParameterCollection`` will be stored on the new class's
``paramCollectionType`` attribute.
For example, when this metaclass is applied to the ``Block`` class it will create a
new class named ``BlockParameterCollection``, and add it as a class attribute called
``Block.paramCollectionType``. The ``BlockParameterCollection`` class will itself be
a subclass of ``ArmiObjectParameterCollection``, which it would have found from the
``Composite`` class from which the ``Block`` class inherits. The ``Composite``
calss, on the other hand, would have obtained the ``ArmiObjectParameterCollection``
from it's parent (``ArmiObject``), since it does not have a ``pDefs`` attribute of
its own.
"""
def __new__(mcl, name, bases, attrs):
assert (
attrs.get("paramCollectionType") is None
), "{} already has paramter collection".format(name)
baseCollections = [
b.paramCollectionType for b in bases if hasattr(b, "paramCollectionType")
]
# Make sure that these are what we expect them to be
assert all(
[
issubclass(c, ParameterCollection)
for c in baseCollections
if c is not None
]
)
# Make sure that we aren't doing some sort of multiple inheritance. We may
# wish to support this in the future, but at this point we don't need it and
# there are probably lots of snakes in that grass.
# Turning this off to support multiple-inheritance materials/matprops material.
# But we should still be careful.
# assert len(baseCollections) <= 1, "Multiple inheritance is not yet supported in the ARMI composite pattern"
# Pull out the one element of the list if it exists
inferredBaseCollection = next(iter(baseCollections), None)
# pDefs can be defined in the class definition; if it is, this is is a Parameter
# Class!
pDefs = attrs.get("pDefs")
makeNewPC = pDefs is not None
if makeNewPC:
# We may have our own parameters, so we need to spin up a new
# XParameterCollection class to store them.
assert isinstance(pDefs, ParameterDefinitionCollection)
collectionName = name + "ParameterCollection"
collectionBase = inferredBaseCollection or ParameterCollection
# Note that we also give a reference to the pDefs to the parameter
# collection. This is so that the ParmameterCollection hierarchy can do all
# of the parameter definitions work, while plugins can associate definitions
# with the ArmiObjects
paramCollectionType = type(
collectionName,
(collectionBase,),
{
"pDefs": pDefs,
},
)
else:
# We will not be defining our own parameters, so we will defer to to those
# of our parent classes if they have any
paramCollectionType = inferredBaseCollection
attrs["paramCollectionType"] = paramCollectionType
nt = type.__new__(mcl, name, bases, attrs)
if makeNewPC:
paramCollectionType._ArmiObject = nt
return nt