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