Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation for POEM_022 - Determining variable shape at runtime based on connections #1671

Merged
merged 35 commits into from Sep 14, 2020

Conversation

naylor-b
Copy link
Member

@naylor-b naylor-b commented Sep 8, 2020

Summary

Implementation PR for POEM_022. Adds the ability to determine variable shapes at runtime based on what those variables are connected to. Also adds a new function hook, setup_partials, that is used to allow a component to compute its partials after all shape information is known.

Backwards incompatibilities

None

New Dependencies

None

@naylor-b naylor-b added the Pending POEM Acceptance Do not merge. This PR is attached to a POEM that is not yet accepted. label Sep 8, 2020
@project-bot project-bot bot added this to In progress in OpenMDAO Dev [Read only] Sep 8, 2020
OpenMDAO Dev [Read only] automation moved this from In progress to Reviewer approved Sep 9, 2020
p = om.Problem()
indep = p.model.add_subsystem('indep', om.IndepVarComp('x1', val=np.ones((2,3))))
indep.add_output('x2', val=np.ones((4,2)))
p.model.add_subsystem('Gdyn', DynShapeGroupSeries(3, 2, DynShapeComp))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested what happens if you accidentally set shape_by_conn to your own name? I imagine you will just get a 'cannot resolve shape' exception, so probably fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, doing a copy_shape using your own name will either result in a failure to resolve, or if that variable happens to be connected to other variables by some other means (shape_by_conn or a resolvable variable connects to it by its own shape_by_conn or copy_shape) then it'll just work anyway. Your comment also reminded me that I didn't have a test for when copy_shape is set to a non-existent variable, so I added that.

@joanibal
Copy link

joanibal commented Sep 9, 2020

Wow! This is a really nice implementation. Thank you for all the work that went into this.

I have just one suggestion. I think it would make it much easier for users to debug their models if they had some graphical representation of the size dependency graph. If there are unresolved variables the error message lists the variables that are an issue in alphabetical order, but doesn't say how they are connected.

With a picture to go along with the error message it is much easier to see the connections and understand why the test test_cycle_unresolved doesn't work.
shape_dependency_graph

This is something that could be done only if the model fails like this...

        if unresolved:
            unresolved = sorted(unresolved)
            #try to save a plot of the dependency graph for debuging
            msg = f"{self.msginfo}: Failed to resolve shapes for {unresolved}."
            try:
                import matplotlib.pyplot as plt
                nx.draw(graph, with_labels=True)
                plt.savefig('shape_dependency_graph.png')
                msg += " See shape_dependency_graph.png for more details"
            except ImportError:
                pass
            except err as e:
                raise err

            raise RuntimeError(msg)

However it is probably better to a have a utility function for this so users could verify that the dependencies are as expected even without an error. I'm not sure how to best implement that, but it sounds possible.

@naylor-b
Copy link
Member Author

@joanibal I added a 'view_dyn_shapes' command to the openmdao command line tool that will display the shape dependency graph using matplotlib, basically like you showed in your example. At some point when I have more time I'd like to replace the matplotlib stuff with a better graph viewing library, but for now, it's at least better than nothing.

@naylor-b naylor-b removed the Pending POEM Acceptance Do not merge. This PR is attached to a POEM that is not yet accepted. label Sep 14, 2020
def add_input(self, name, val=1.0, shape=None, src_indices=None, flat_src_indices=None,
units=None, desc='', tags=None):
def add_input(self, name, val=None, shape=None, src_indices=None, flat_src_indices=None,
units=None, desc='', tags=None, shape_by_conn=False, copy_shape=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: changing the default value is a backward incompatibility?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted this back to 1.0. So now, val is allowed to be set but it must be a scalar, and once the shape is known, that scalar is used to fill the array.

declared here since this is called after all size/shape information is known for
all variables.
"""
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: changing the API for where partials are declared is a backward incompatibility?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not backwardly incompatible. Doing it in the old way still works, although we recommend that people do it now in setup_partials for consistency sake.

@@ -1346,6 +1364,7 @@ def _setup_global_shapes(self):
"""
meta = self._var_allprocs_abs2meta
loc_meta = self._var_abs2meta
myrank = self.comm.rank
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

self.add_output('out', shape=3)

def compute(self, inputs, outputs):
outputs['out'] *= self.comm.rank
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is strange, could you explain what's going on here (computing output based on itself)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this so the output now depends upon the input.

@@ -22,7 +22,7 @@ Here is an example, based on the :ref:`Betz Limit Example <betz_limit_tutorial>`


The calls to :code:`declare_partials` tell OpenMDAO which partial derivatives to expect.
This is always done inside the :code:`setup` method.
This should always be done inside the :code:`setup_partials` method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should or must?
if should then more explanation is probably required...
if must then we do we need a deprecation or exception when it's done the old way?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more explanation about why setup_partials is the recommended way.

@swryan swryan merged commit c03985f into OpenMDAO:master Sep 14, 2020
OpenMDAO Dev [Read only] automation moved this from Reviewer approved to Done Sep 14, 2020
@naylor-b naylor-b deleted the dyn_shape branch March 8, 2022 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

5 participants