Documenting Models

Introduction

This is the third tutorial in the Intro Series where we do a deeper dive into the topic of documenting models. One of the underlying principles of the Footings framework is models should be self documenting.

To start, it is important to note that the Footings framework supports numpy style docstrings and internally builds off numpydoc to generate docstrings and the associated objects for generating documentation in sphinx.

Documented Model

Below is the code for AddABC from the prior two tutorials with documentations.

from footings.model import (
    model,
    step,
    def_parameter,
    def_intermediate,
    def_return,
)

@model(steps=["_add_a_b", "_add_ab_c"])
class AddABC:
    """This model takes 3 parameters - a, b, and c and adds them together in two steps."""
    a = def_parameter(dtype=int, description="This is parameter a.")
    b = def_parameter(dtype=int, description="This is parameter b.")
    c = def_parameter(dtype=int, description="This is parameter c.")
    ab = def_intermediate(dtype=int, description="This holds a + b.")
    abc = def_return(dtype=int, description="The sum of ab and c.")

    @step(uses=["a", "b"], impacts=["ab"])
    def _add_a_b(self):
        """Add a and b together and assign to ab."""
        self.ab = self.a + self.b

    @step(uses=["ab", "c"], impacts=["abc"])
    def _add_ab_c(self):
        """Add ab and c together for final return."""
        self.abc = self.ab + self.c

As the above code shows -

  • Documentation about the model is added as a standard python docstring directly underneath the class.

  • Documentation for attributes, including the data types, should be set within the def_* function.

  • Information on each step can be added as a docstring underneath the method defining that step.

Do note the attributes could be documented in the associated docstring. However, it is recommended to put the attribute documentation within the def_* function. Lastly, being able to pass documentation (i.e., dtype and description) into functions as code allows us to move the source of the documentation to be elsewhere.

The below code demonstrates this. Assume the model developer wants to have a data dictionary file that is the true source of information underlying the parameters.

import yaml

data_dictionary_file = """
a:
  dtype: int
  description: This is parameter a.
b:
  dtype: int
  description: This is parameter b.
c:
  dtype: int
  description: This is parameter c.
"""

data_dictionary = yaml.safe_load(data_dictionary_file)

data_dictionary
{'a': {'dtype': 'int', 'description': 'This is parameter a.'},
 'b': {'dtype': 'int', 'description': 'This is parameter b.'},
 'c': {'dtype': 'int', 'description': 'This is parameter c.'}}

We can then pass the information from the data dictionary file into the associated parameters.

@model(steps=["_add_a_b", "_add_ab_c"])
class AddABC:
    """This model takes 3 parameters - a, b, and c and adds them together in two steps."""
    a = def_parameter(**data_dictionary["a"])
    b = def_parameter(**data_dictionary["b"])
    c = def_parameter(**data_dictionary["c"])
    ab = def_intermediate(dtype=int, description="This holds a + b.")
    abc = def_return(dtype=int, description="The sum of ab and c.")

    @step(uses=["a", "b"], impacts=["ab"])
    def _add_a_b(self):
        """Add a and b together and assign to ab."""
        self.ab = self.a + self.b

    @step(uses=["ab", "c"], impacts=["abc"])
    def _add_ab_c(self):
        """Add ab and c together for final return."""
        self.abc = self.ab + self.c

Viewing the Documentation

To see what the documentation looks like to a user call help on the model.

help(AddABC)
Help on class AddABC in module footings.model:

class AddABC(__main__.AddABC, Model)
 |  AddABC(*, a: 'int', b: 'int', c: 'int') -> None
 |  
 |  This model takes 3 parameters - a, b, and c and adds them together in two steps.
 |  
 |  .. rubric:: Parameters
 |  
 |  - **a (int)** - This is parameter a.
 |  - **b (int)** - This is parameter b.
 |  - **c (int)** - This is parameter c.
 |  
 |  .. rubric:: Intermediates
 |  
 |  - **ab (int)** - This holds a + b.
 |  
 |  .. rubric:: Returns
 |  
 |  - **abc (int)** - The sum of ab and c.
 |  
 |  .. rubric:: Steps
 |  
 |  1) **_add_a_b** - Add a and b together and assign to ab.
 |  2) **_add_ab_c** - Add ab and c together for final return.
 |  
 |  Method resolution order:
 |      AddABC
 |      __main__.AddABC
 |      Model
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __eq__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __ge__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __getstate__ = slots_getstate(self)
 |      Automatically created by attrs.
 |  
 |  __gt__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __init__(self, *, a: 'int', b: 'int', c: 'int') -> None
 |      Method generated by attrs for class AddABC.
 |  
 |  __le__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __lt__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __ne__(self, other)
 |      Method generated by attrs for class AddABC.
 |  
 |  __setattr__(self, name, val)
 |      Method generated by attrs for class AddABC.
 |  
 |  __setstate__ = slots_setstate(self, state)
 |      Automatically created by attrs.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  a
 |      Intermediate representation of attributes that uses a counter to preserve
 |      the order in which the attributes have been defined.
 |      
 |      *Internal* data structure of the attrs library.  Running into is most
 |      likely the result of a bug like a forgotten `@attr.s` decorator.
 |  
 |  ab
 |      Intermediate representation of attributes that uses a counter to preserve
 |      the order in which the attributes have been defined.
 |      
 |      *Internal* data structure of the attrs library.  Running into is most
 |      likely the result of a bug like a forgotten `@attr.s` decorator.
 |  
 |  abc
 |      Intermediate representation of attributes that uses a counter to preserve
 |      the order in which the attributes have been defined.
 |      
 |      *Internal* data structure of the attrs library.  Running into is most
 |      likely the result of a bug like a forgotten `@attr.s` decorator.
 |  
 |  b
 |      Intermediate representation of attributes that uses a counter to preserve
 |      the order in which the attributes have been defined.
 |      
 |      *Internal* data structure of the attrs library.  Running into is most
 |      likely the result of a bug like a forgotten `@attr.s` decorator.
 |  
 |  c
 |      Intermediate representation of attributes that uses a counter to preserve
 |      the order in which the attributes have been defined.
 |      
 |      *Internal* data structure of the attrs library.  Running into is most
 |      likely the result of a bug like a forgotten `@attr.s` decorator.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __attrs_attrs__ = (Attribute(name='__model_steps__', default=NOTHI...e...
 |  
 |  __attrs_own_setattr__ = True
 |  
 |  __hash__ = None
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from __main__.AddABC:
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from __main__.AddABC:
 |  
 |  __model_attribute_map__ = {'a': 'parameter.a', 'ab': 'intermediate.ab'...
 |  
 |  __model_intermediates__ = ('ab',)
 |  
 |  __model_meta__ = ()
 |  
 |  __model_parameters__ = ('a', 'b', 'c')
 |  
 |  __model_returns__ = ('abc',)
 |  
 |  __model_sensitivities__ = ()
 |  
 |  __model_steps__ = ('_add_a_b', '_add_ab_c')
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from Model:
 |  
 |  audit(self, file: str = None, **kwargs)
 |      Audit the model which returns copies of the object as it is modified across each step.
 |      
 |      :param Union[str, None] file: The name of the audit output file.
 |      :param kwargs: Additional key words passed to audit.
 |      
 |      :return: If file is None, an AuditContainer else an audit file in specfified format (e.g., .xlsx).
 |  
 |  run(self, to_step=None)
 |      Runs the model and returns any returns defined.
 |      
 |      :param Union[str, None]: The name of the step to run model through.
 |  
 |  visualize(self)
 |      Visualize the model to get an understanding of what model attributes are used and when.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from Model:
 |  
 |  __annotations__ = {'__model_attribute_map__': <class 'dict'>, '__model...

When viewing the output, you will see the attributes are split into sections based on the def_* function called. In addition, directly below the return detail is a section titled Steps with the method name of the step and the description of the step.

This layout of the documentation will flow through if using sphinx. Be sure to include footings.doc_tools as an extension in your conf.py file. For an example of this see the footings-idi-model repository and documentation.

Closing

When building a model using the Footings framework, the developer needs to document the different components of the model and the footings library will organize those components into consumable documentation. Model documentation is paired best with sphinx and a CI/CD pipeline producing versioned documentation along side your model code.