Skip to content

Commit

Permalink
Merge pull request #1656 from Kenneth-T-Moore/ken3
Browse files Browse the repository at this point in the history
Added a new iteration counter for apply_nonlinear.
  • Loading branch information
swryan committed Aug 28, 2020
2 parents 42f1d7d + ddf9cf6 commit e6dd0ea
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 38 deletions.
35 changes: 19 additions & 16 deletions openmdao/core/explicitcomponent.py
Expand Up @@ -224,25 +224,26 @@ def _apply_nonlinear(self):
"""
outputs = self._outputs
residuals = self._residuals
with Recording(self.pathname + '._apply_nonlinear', self.iter_count, self):
with self._unscaled_context(outputs=[outputs], residuals=[residuals]):
residuals.set_vec(outputs)
with self._unscaled_context(outputs=[outputs], residuals=[residuals]):
residuals.set_vec(outputs)

# Sign of the residual is minus the sign of the output vector.
residuals *= -1.0
# Sign of the residual is minus the sign of the output vector.
residuals *= -1.0

self._inputs.read_only = True
try:
if self._discrete_inputs or self._discrete_outputs:
self.compute(self._inputs, self._outputs, self._discrete_inputs,
self._discrete_outputs)
else:
self.compute(self._inputs, self._outputs)
finally:
self._inputs.read_only = False
self._inputs.read_only = True
try:
if self._discrete_inputs or self._discrete_outputs:
self.compute(self._inputs, self._outputs, self._discrete_inputs,
self._discrete_outputs)
else:
self.compute(self._inputs, self._outputs)
finally:
self._inputs.read_only = False

residuals += outputs
outputs -= residuals
residuals += outputs
outputs -= residuals

self.iter_count_apply += 1

def _solve_nonlinear(self):
"""
Expand All @@ -261,6 +262,8 @@ def _solve_nonlinear(self):
finally:
self._inputs.read_only = False

# Iteration counter is incremented in the Recording context manager at exit.

def _apply_linear(self, jac, vec_names, rel_systems, mode, scope_out=None, scope_in=None):
"""
Compute jac-vec product. The model is assumed to be in a scaled state.
Expand Down
9 changes: 6 additions & 3 deletions openmdao/core/group.py
Expand Up @@ -2059,9 +2059,10 @@ def _apply_nonlinear(self):

self._transfer('nonlinear', 'fwd')
# Apply recursion
with Recording(name + '._apply_nonlinear', self.iter_count, self):
for subsys in self._subsystems_myproc:
subsys._apply_nonlinear()
for subsys in self._subsystems_myproc:
subsys._apply_nonlinear()

self.iter_count_apply += 1

def _solve_nonlinear(self):
"""
Expand All @@ -2072,6 +2073,8 @@ def _solve_nonlinear(self):
with Recording(name + '._solve_nonlinear', self.iter_count, self):
self._nonlinear_solver.solve()

# Iteration counter is incremented in the Recording context manager at exit.

def _guess_nonlinear(self):
"""
Provide initial guess for states.
Expand Down
23 changes: 13 additions & 10 deletions openmdao/core/implicitcomponent.py
Expand Up @@ -66,16 +66,17 @@ def _apply_nonlinear(self):
Compute residuals. The model is assumed to be in a scaled state.
"""
with self._unscaled_context(outputs=[self._outputs], residuals=[self._residuals]):
with Recording(self.pathname + '._apply_nonlinear', self.iter_count, self):
self._inputs.read_only = self._outputs.read_only = True
try:
if self._discrete_inputs or self._discrete_outputs:
self.apply_nonlinear(self._inputs, self._outputs, self._residuals,
self._discrete_inputs, self._discrete_outputs)
else:
self.apply_nonlinear(self._inputs, self._outputs, self._residuals)
finally:
self._inputs.read_only = self._outputs.read_only = False
self._inputs.read_only = self._outputs.read_only = True
try:
if self._discrete_inputs or self._discrete_outputs:
self.apply_nonlinear(self._inputs, self._outputs, self._residuals,
self._discrete_inputs, self._discrete_outputs)
else:
self.apply_nonlinear(self._inputs, self._outputs, self._residuals)
finally:
self._inputs.read_only = self._outputs.read_only = False

self.iter_count_apply += 1

def _solve_nonlinear(self):
"""
Expand All @@ -98,6 +99,8 @@ def _solve_nonlinear(self):
finally:
self._inputs.read_only = False

# Iteration counter is incremented in the Recording context manager at exit.

def _guess_nonlinear(self):
"""
Provide initial guess for states.
Expand Down
20 changes: 15 additions & 5 deletions openmdao/core/system.py
Expand Up @@ -134,11 +134,16 @@ class System(object):
under_approx : bool
When True, this system is undergoing approximation.
iter_count : int
Int that holds the number of times this system has iterated
in a recording run.
Counts the number of times this system has called _solve_nonlinear. This also
corresponds to the number of times that the system's outputs are recorded if a recorder
is present.
iter_count_apply : int
Counts the number of times the system has called _apply_nonlinear. For ExplicitComponent,
calls to apply_nonlinear also call compute, so number of executions can be found by adding
this and iter_count together. Recorders do no record calls to apply_nonlinear.
iter_count_without_approx : int
Int that holds the number of times this system has iterated
in a recording run excluding any calls due to approximation schemes.
Counts the number of times the system has iterated but excludes any that occur during
approximation of derivatives.
cite : str
Listing of relevant citations that should be referenced when
publishing work that uses this class.
Expand Down Expand Up @@ -366,8 +371,9 @@ def __init__(self, num_par_fd=1, **kwargs):

self._problem_meta = None

# Case recording related
# Counting iterations.
self.iter_count = 0
self.iter_count_apply = 0
self.iter_count_without_approx = 0

self.cite = ""
Expand Down Expand Up @@ -3871,6 +3877,7 @@ def record_iteration(self):

self._rec_mgr.record_iteration(self, data, metadata)

# All calls to _solve_nonlinear are recorded, The counter is incremented after recording.
self.iter_count += 1
if not self.under_approx:
self.iter_count_without_approx += 1
Expand Down Expand Up @@ -3900,6 +3907,9 @@ def _reset_iter_counts(self):
"""
for s in self.system_iter(include_self=True, recurse=True):
s.iter_count = 0
s.iter_count_apply = 0
s.iter_count_without_approx = 0

if s._linear_solver:
s._linear_solver._iter_count = 0
if s._nonlinear_solver:
Expand Down
3 changes: 2 additions & 1 deletion openmdao/core/tests/test_approx_derivs.py
Expand Up @@ -111,7 +111,8 @@ def compute(self, inputs, outputs):
# 1. run_model; 2. step x; 3. step y
self.assertEqual(model.parab.count, 3)
self.assertEqual(model.parab.iter_count_without_approx, 1)
self.assertEqual(model.parab.iter_count, 3)
self.assertEqual(model.parab.iter_count, 1)
self.assertEqual(model.parab.iter_count_apply, 2)

def test_fd_count_driver(self):
# Make sure we aren't doing FD wrt any var that isn't in the driver desvar set.
Expand Down
39 changes: 36 additions & 3 deletions openmdao/core/tests/test_expl_comp.py
Expand Up @@ -6,12 +6,14 @@
import numpy as np

import openmdao.api as om
from openmdao.test_suite.components.double_sellar import SubSellar
from openmdao.test_suite.components.expl_comp_simple import TestExplCompSimple, \
TestExplCompSimpleDense
from openmdao.utils.assert_utils import assert_near_equal
from openmdao.utils.general_utils import printoptions, remove_whitespace
from openmdao.utils.mpi import MPI
from openmdao.test_suite.components.double_sellar import SubSellar
from openmdao.test_suite.components.expl_comp_simple import TestExplCompSimple, \
TestExplCompSimpleDense
from openmdao.test_suite.components.sellar import SellarDis1withDerivatives, \
SellarDis2withDerivatives

# Note: The following class definitions are used in feature docs

Expand Down Expand Up @@ -957,6 +959,37 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode):
# verify read_only status is reset after AnalysisError
prob['length'] = 111.

def test_iter_count(self):
# Make sure we correctly count iters in both _apply_nonlinear and _solve_nonlinear
class SellarMDF(om.Group):
def setup(self):
self.set_input_defaults('x', 1.0)
self.set_input_defaults('z', np.array([5.0, 2.0]))

cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])
cycle.add_subsystem('d1', SellarDis1withDerivatives(), promotes_inputs=['x', 'z', 'y2'],
promotes_outputs=['y1'])
cycle.add_subsystem('d2', SellarDis2withDerivatives(), promotes_inputs=['z', 'y1'],
promotes_outputs=['y2'])

cycle.linear_solver = om.ScipyKrylov()

cycle.nonlinear_solver = om.NewtonSolver(solve_subsystems=False)


prob = om.Problem()
prob.model = SellarMDF()

prob.setup()
prob.set_solver_print(level=0)

prob.run_model()
self.assertEqual(prob.model.cycle.d1.iter_count, 0)
self.assertEqual(prob.model.cycle.d2.iter_count, 0)
self.assertEqual(prob.model.cycle.d1.iter_count_apply, 10)
self.assertEqual(prob.model.cycle.d2.iter_count_apply, 10)


@unittest.skipUnless(MPI, "MPI is required.")
class TestMPIExplComp(unittest.TestCase):
N_PROCS = 3
Expand Down
1 change: 1 addition & 0 deletions openmdao/docs/theory_manual/index.rst
Expand Up @@ -18,6 +18,7 @@ mathematical manner to help users gain a deeper understanding of how the framewo
setup_stack.rst
solver_api.rst
scaling.rst
iter_count.rst


Total Derivatives Theory
Expand Down
35 changes: 35 additions & 0 deletions openmdao/docs/theory_manual/iter_count.rst
@@ -0,0 +1,35 @@
************************************************
Determining How Many Times a System was Executed
************************************************

All OpenMDAO `Systems` have a set of local counters that keep track of how many times they have
been executed.

**iter_count**

Counts the number of times this system has called _solve_nonlinear. This also
corresponds to the number of times that the system's outputs are recorded if a recorder
is present.

**iter_count_apply**

Counts the number of times the system has called _apply_nonlinear. For ExplicitComponent,
calls to apply_nonlinear also call compute, so number of executions can be found by adding
this and iter_count together. Recorders do no record calls to _apply_nonlinear.

**iter_count_without_approx**

Counts the number of times the system has iterated but excludes any that occur during
approximation of derivatives.

When you have an `ExplicitComponent`, the number stored in iter_count may not match the total
number of times that the "compute" function has been called. This is because compute is also
called whenever '_apply_nonlinear' is called to compute the norm of the current residual. For
an explicit equation, the residual is defined as the difference in the value of the outputs
before and after execution, and an additional execution is required to compute this.

The correct execution count for an ExplicitComponent can always be obtained by adding iter_count
and iter_count_apply.

The recorder iteration coordinate will always match the iter_count because calls to apply_nonlinear
are not recorded.

0 comments on commit e6dd0ea

Please sign in to comment.