Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

jcapriot
Copy link
Member

@jcapriot jcapriot commented Aug 16, 2024

Summary

Adds the ability to inherit information from a parent class's documentation and correspondingly update the class's call signature.

PR Checklist

  • If this is a work in progress PR, set as a Draft PR
  • Linted my code according to the style guides.
  • Added tests to verify changes to the code.
  • Added necessary documentation to any new functions/classes following the
    expect style.
  • Marked as ready for review (if this is was a draft PR), and converted
    to a Pull Request
  • Tagged @simpeg/simpeg-developers when ready for review.

Reference issue

Relevant to #1420

What does this implement/fix?

Documentation implementation

Adds a mechanism to inherit documentation descriptions from parent classes. This avoids us having to copy and paste documentation code for each parameter into every single subclass. For example, we can write the description of the mesh parameter in one place, then make use of that in every child class. Or, we can access all of the other parameters that are less likely to be used from a parent class. It also then updates the call signature of the class's __init__ function to match the description in the documentation.

As of right now, my proposed syntax for this is demonstrated in the few classes I've added this to.

The docstring for LinearSimulation as coded:
https://github.com/jcapriot/simpeg/blob/17f934d7b25e7669dd483788f50f366b63ad224c/simpeg/simulation.py#L717-L758

Essentially the %(....) triggers a replacement on the docstring when the @doc_inherit() decorator is attached to the class. Specifically a %(super.item) triggers a lookup for the description of an argument named item in the next class that describes item in the class's inheritance tree. %(module.ClassName.item) would trigger a lookup for item specifically in the importable class module.ClassName, and %(super.*) would trigger an inclusion of every parameter from the class's inheritance tree (that is not already documented on this class already (you could also trigger this from a specific class as %(module.ClassName.*)). There is also an option to specifically exclude parameters from the star include operation, as seen in the LinearSimulation excluding the solver and solver_opts parameters.

After the replacements the LinearSimulation docstring looks like:

"""Linear forward simulation class.

The ``LinearSimulation`` class is used to define forward simulations of the form:

.. math::
    \mathbf{d} = \mathbf{G \, f}(\mathbf{m})

where :math:`\mathbf{m}` are the model parameters, :math:`\mathbf{f}` is a
mapping operator (optional) from the model space to a user-defined parameter space,
:math:`\mathbf{d}` is the predicted data vector, and :math:`\mathbf{G}` is an
``(n_data, n_param)`` linear operator.

The ``LinearSimulation`` class is generally used as a base class that is inherited by
other simulation classes within SimPEG. However, it can be used directly as a
simulation class if the :py:attr:`G` property is used to set the linear forward
operator directly.

By default, we assume the mapping operator :math:`\mathbf{f}` is the identity map,
and that the forward simulation reduces to:

.. math::
    \mathbf{d} = \mathbf{G \, m}

Parameters
----------
mesh : discretize.base.BaseMesh, optional
    Mesh on which the forward problem is discretized.
model_map : simpeg.maps.BaseMap
    Mapping from the model parameters to vector that the linear operator acts on.
G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx
    The linear operator. For a ``model_map`` that maps within the same vector space
    (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters.
    If not, the dimension ``n_param`` of the linear operator will depend on the mapping.
model : (n_m,) array_like, optional
    The parameter model, often used to descibe the model in an inversion.
    If there are any physical property maps assigned, those physical properties
    will be linked to this model through the map, and accessing them will require
    a model to be set.
survey : simpeg.survey.BaseSurvey, optional
    The survey for the simulation.
sensitivity_path : str, optional
    Path to directory where sensitivity file is stored.
counter : None or simpeg.utils.Counter
    SimPEG ``Counter`` object to store iterations and run-times.
verbose : bool, default: False
    Verbose progress printout.
"""

Because we have included a %(super.*) lookup, it doesn't include a **kwargs description.

Call signatures

Updating the documentation is nice... But it doesn't enable tab completion on most editors, or type inference, and it doesn't make the default values for optional arguments shown in the call signature. But that's the key there, you can specifically set a function's __signature__ property to change it's call signature.
So the next bit of this replacement appropriately updates the functions call signature to add the items that were included from elsewhere (that were not already part of it's call signature). This parameters are only allowed as keyword arguments, and are marked as such when added to the call signature. They are added in the order they are added to the documentation.

Now inspecting the loaded LinearSimulation class in a jupyter notebook we see it's description as:
image

Tab completion in a notebook shows all of the added parameters:
image

The generated documentation page:
image

Additional information

This relies on the classes implementing the numpydoc style doc strings. It will search for descriptions in parents that are described in either the Paramaters or Other Parameters sections.

This spirit of this was inspired by matplotlib, where they automatically populate their docstrings with the multitude of parameters that their classes take.

Unfortunately this does not (and cannot) effect IDE's that perform a static analysis of the written code (i.e. pycharm). But the interactive functionality is very welcome, and the static analysis is not any different than it currently is. We could generate .pyi files for these classes using a utility function (that could be hooked into pre-commit?) that would enable type inspection for static analyzers. I haven't looked into this too much though, just quickly tested it as a proof of concept that pycharm would look at the .pyi file and pull the signature and documentation from there (which it does).

With the amount of times I felt like I wanted to add the decorator to classes, it might be better served as a metaclass that all simpeg classes will have access to.

Copy link

codecov bot commented Aug 22, 2024

Codecov Report

Attention: Patch coverage is 80.23256% with 34 lines in your changes missing coverage. Please review.

Project coverage is 42.57%. Comparing base (c699a84) to head (a6dbd21).
Report is 3 commits behind head on main.

Files Patch % Lines
simpeg/base/base.py 80.39% 18 Missing and 12 partials ⚠️
simpeg/base/pde.py 33.33% 2 Missing and 2 partials ⚠️

❗ There is a different number of reports uploaded between BASE (c699a84) and HEAD (a6dbd21). Click for more details.

HEAD has 25 uploads less than BASE
Flag BASE (c699a84) HEAD (a6dbd21)
30 5
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #1518       +/-   ##
===========================================
- Coverage   86.24%   42.57%   -43.67%     
===========================================
  Files         396      345       -51     
  Lines       50329    44265     -6064     
  Branches     5308     4971      -337     
===========================================
- Hits        43405    18847    -24558     
- Misses       5473    24292    +18819     
+ Partials     1451     1126      -325     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jcapriot
Copy link
Member Author

I've been thinking a bit more about where this functionality should live, and it seems like it really should be it's own package, mostly because I want to use its functionality independent of SimPEG. Any thoughts on a name?

docerator has been living in my mind as a portmanteau of documentation and generator (and it sounds like decorator).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.