Debugging solvers – a hands-on set of problems

Level: Advanced

Topics: Model construction and debugging solving

Let’s work through some solver issues together! Each one of these setups will have some sort of problem or potential problem and I want you to find a solution or fix.

Circuit example with a Newton solver

Here we have an example system entirely defined in the notebook. Then we try to converge it using a Newton solver but it fails. What’s going on here? How would you tackle this? Please modify this code so that the solver converges.

Make sure to check out the solver debugging checklist and to use the tips from there.

import numpy as np

import openmdao.api as om


class Resistor(om.ExplicitComponent):
    """Computes current across a resistor using Ohm's law."""

    def initialize(self):
        self.options.declare('R', default=1., desc='Resistance in Ohms')

    def setup(self):
        self.add_input('V_in', units='V')
        self.add_input('V_out', units='V')
        self.add_output('I', units='A')

        # partial derivs are constant, so we can assign their values in setup
        R = self.options['R']
        self.declare_partials('I', 'V_in', val=1 / R)
        self.declare_partials('I', 'V_out', val=-1 / R)

    def compute(self, inputs, outputs):
        deltaV = inputs['V_in'] - inputs['V_out']
        outputs['I'] = deltaV / self.options['R']

class Diode(om.ExplicitComponent):
    """Computes current across a diode using the Shockley diode equation."""

    def initialize(self):
        self.options.declare('Is', default=1e-15, desc='Saturation current in Amps')
        self.options.declare('Vt', default=.025875, desc='Thermal voltage in Volts')

    def setup(self):
        self.add_input('V_in', units='V')
        self.add_input('V_out', units='V')
        self.add_output('I', units='A')

        # non-linear component, so we'll declare the partials here but compute them in compute_partials
        self.declare_partials('I', 'V_in')
        self.declare_partials('I', 'V_out')

    def compute(self, inputs, outputs):
        deltaV = inputs['V_in'] - inputs['V_out']
        Is = self.options['Is']
        Vt = self.options['Vt']
        outputs['I'] = Is * (np.exp(deltaV / Vt) - 1)

    def compute_partials(self, inputs, J):
        deltaV = inputs['V_in'] - inputs['V_out']
        Is = self.options['Is']
        Vt = self.options['Vt']
        I = Is * np.exp(deltaV / Vt)

        J['I', 'V_in'] = I/Vt
        J['I', 'V_out'] = -I/Vt

class Node(om.ImplicitComponent):
    """Computes voltage residual across a node based on incoming and outgoing current."""

    def initialize(self):
        self.options.declare('n_in', default=1, types=int, desc='number of connections with + assumed in')
        self.options.declare('n_out', default=1, types=int, desc='number of current connections + assumed out')

    def setup(self):
        self.add_output('V', val=5., units='V')

        for i in range(self.options['n_in']):
            i_name = 'I_in:{}'.format(i)
            self.add_input(i_name, units='A')
            self.declare_partials('V', i_name, val=1)

        for i in range(self.options['n_out']):
            i_name = 'I_out:{}'.format(i)
            self.add_input(i_name, units='A')
            self.declare_partials('V', i_name, val=-1)

            # note: we don't declare any partials wrt `V` here,
            #      because the residual doesn't directly depend on it

    def apply_nonlinear(self, inputs, outputs, residuals):
        residuals['V'] = 0.
        for i_conn in range(self.options['n_in']):
            residuals['V'] += inputs['I_in:{}'.format(i_conn)]
        for i_conn in range(self.options['n_out']):
            residuals['V'] -= inputs['I_out:{}'.format(i_conn)]

class Circuit(om.Group):

    def setup(self):
        self.add_subsystem('n1', Node(n_in=1, n_out=2), promotes_inputs=[('I_in:0', 'I_in')])
        self.add_subsystem('n2', Node())  # leaving defaults

        self.add_subsystem('R1', Resistor(R=100.), promotes_inputs=[('V_out', 'Vg')])
        self.add_subsystem('R2', Resistor(R=10000.))
        self.add_subsystem('D1', Diode(), promotes_inputs=[('V_out', 'Vg')])

        self.connect('n1.V', ['R1.V_in', 'R2.V_in'])
        self.connect('R1.I', 'n1.I_out:0')
        self.connect('R2.I', 'n1.I_out:1')

        self.connect('n2.V', ['R2.V_out', 'D1.V_in'])
        self.connect('R2.I', 'n2.I_in:0')
        self.connect('D1.I', 'n2.I_out:0')

        self.nonlinear_solver = om.NewtonSolver()

        self.nonlinear_solver.options['iprint'] = 2
        self.nonlinear_solver.options['maxiter'] = 10
        self.nonlinear_solver.options['solve_subsystems'] = True


p = om.Problem()
model = p.model

model.add_subsystem('circuit', Circuit())

p.setup()

p.set_val('circuit.I_in', 0.1)
p.set_val('circuit.Vg', 0.)

# set some initial guesses
p.set_val('circuit.n1.V', 10.)
p.set_val('circuit.n2.V', 1e-3)

p.run_model()
=======
circuit
=======
NL: Newton 0 ; 0.00141407214 1
NL: Newton 1 ; 0.0014214429 1.00521244
NL: Newton 2 ; 0.00142899707 1.01055457
NL: Newton 3 ; 0.00143666458 1.01597687
NL: Newton 4 ; 0.00144444604 1.02147974
NL: Newton 5 ; 0.00145234276 1.02706412
NL: Newton 6 ; 0.00146035606 1.03273095
NL: Newton 7 ; 0.0014684873 1.03848118
NL: Newton 8 ; 0.00147673781 1.04431575
NL: Newton 9 ; 0.00148510896 1.05023564
NL: Newton 10 ; 0.00149360212 1.05624181
NL: NewtonSolver 'NL: Newton' on system 'circuit' failed to converge in 10 iterations.

Simple implicit calculation doesn’t converge

Here we have a new system based on Kepler’s Equation. The solver isn’t able to converge with this setup. What can you change to get the model to converge?

import numpy as np

import openmdao.api as om

prob = om.Problem()

bal = om.BalanceComp()

bal.add_balance(name='E', val=0.0, units='rad', eq_units='rad', rhs_name='M')

# Use M (mean anomaly) as the initial guess for E (eccentric anomaly)
def guess_function(inputs, outputs, residuals):
    if np.abs(residuals['E']) > 1.0E-2:
        outputs['E'] = inputs['M']

bal.options['guess_func'] = guess_function

# ExecComp used to compute the LHS of Kepler's equation.
lhs_comp = om.ExecComp('lhs=E - ecc * sin(E)',
                       lhs={'units': 'rad'},
                       E={'units': 'rad'},
                       ecc={'units': None})

prob.model.set_input_defaults('M', 85.0, units='deg')

prob.model.add_subsystem(name='lhs_comp', subsys=lhs_comp,
                         promotes_inputs=['E', 'ecc'])

prob.model.add_subsystem(name='balance', subsys=bal,
                         promotes_inputs=['M'],
                         promotes_outputs=['E'])

# Explicit connections
prob.model.connect('lhs_comp.lhs', 'balance.lhs:E')

# Set up solvers
prob.model.linear_solver = om.LinearBlockGS()
prob.model.nonlinear_solver = om.NewtonSolver(solve_subsystems=False, maxiter=10, iprint=2)

prob.setup()

prob.set_val('M', 85.0, units='deg')
prob.set_val('E', 85.0, units='deg')
prob.set_val('ecc', 0.6)

prob.run_model()

M = prob.get_val('M')
E = prob.get_val('E')
print(f'M = {M}')
print(f'E = {E}')
NL: Newton 0 ; 0.3321557 1
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 1 ; 0.237871778 0.716145404
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 2 ; 0.138431564 0.41676709
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 3 ; 0.078332712 0.235831305
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 4 ; 0.0434105989 0.130693524
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 5 ; 0.0233532448 0.070308126
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 6 ; 0.0121412104 0.0365527685
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 7 ; 0.00610725685 0.0183867291
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 8 ; 0.00298686556 0.00899236582
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 9 ; 0.00142908564 0.00430245707
|  LN: LNBGSSolver 'LN: LNBGS' on system '' failed to converge in 10 iterations.
NL: Newton 10 ; 0.000672860219 0.00202573739
NL: NewtonSolver 'NL: Newton' on system '' failed to converge in 10 iterations.
M = [1.48352986]
E = [2.02265228]

Propulsor Newton solver doesn’t converge

This next problem uses the pyCycle engine modeling tool to create complex realistic problems. You will need to install pyCycle to work through this notebook. You can do so with pip install om-pycycle.

You don’t strictly need to understand pyCycle to find value from this notebook. Of course if you understand the physics, it might be easier to understand the solver hierarchy and why the model is set up that way. But if you don’t understand it, then you can view the states and residuals just as numbers without a particular physical meaning.

Let’s first start with a case where a Newton solver doesn’t converge. First we’ll define some pyCycle groups for a propulsor.

import openmdao.api as om
import pycycle.api as pyc


class Propulsor(pyc.Cycle):
    def setup(self):
        design = self.options['design']

        USE_TABULAR = True
        if USE_TABULAR: 
            self.options['thermo_method'] = 'TABULAR'
            self.options['thermo_data'] = pyc.AIR_JETA_TAB_SPEC
        else: 
            self.options['thermo_method'] = 'CEA'
            self.options['thermo_data'] = pyc.species_data.janaf
            FUEL_TYPE = 'JP-7'

        self.add_subsystem('fc', pyc.FlightConditions())

        self.add_subsystem('inlet', pyc.Inlet())
        self.add_subsystem('fan', pyc.Compressor(map_data=pyc.FanMap, map_extrap=True))
        self.add_subsystem('nozz', pyc.Nozzle())
        
        self.add_subsystem('perf', pyc.Performance(num_nozzles=1, num_burners=0))

        balance = om.BalanceComp()
        if design:
            self.add_subsystem('shaft', om.IndepVarComp('Nmech', 1., units='rpm'))
            self.connect('shaft.Nmech', 'fan.Nmech')

            balance.add_balance('W', units='lbm/s', eq_units='hp', val=50., lower=1., upper=500.)
            self.add_subsystem('balance', balance,
                               promotes_inputs=[('rhs:W', 'pwr_target')])
            self.connect('fan.power', 'balance.lhs:W')

        else:
            # vary mass flow till the nozzle area matches the design values
            balance.add_balance('W', units='lbm/s', eq_units='inch**2', val=50, lower=1., upper=500.)
            self.connect('nozz.Throat:stat:area', 'balance.lhs:W')

            balance.add_balance('Nmech', val=1., units='rpm', lower=0.1, upper=2.0, eq_units='hp')
            self.connect('balance.Nmech', 'fan.Nmech')
            self.connect('fan.power', 'balance.lhs:Nmech')

            self.add_subsystem('balance', balance,
                               promotes_inputs=[('rhs:Nmech', 'pwr_target')])

        self.pyc_connect_flow('fc.Fl_O', 'inlet.Fl_I')
        self.pyc_connect_flow('inlet.Fl_O', 'fan.Fl_I')
        self.pyc_connect_flow('fan.Fl_O', 'nozz.Fl_I')


        self.connect('fc.Fl_O:stat:P', 'nozz.Ps_exhaust')
        self.connect('inlet.Fl_O:tot:P', 'perf.Pt2')
        self.connect('fan.Fl_O:tot:P', 'perf.Pt3')
        self.connect('inlet.F_ram', 'perf.ram_drag')
        self.connect('nozz.Fg', 'perf.Fg_0')

        self.connect('balance.W', 'fc.W')

        newton = self.nonlinear_solver = om.NewtonSolver()
        newton.options['atol'] = 1e-12
        newton.options['rtol'] = 1e-12
        newton.options['iprint'] = 2
        newton.options['maxiter'] = 10
        newton.options['solve_subsystems'] = True
        newton.options['max_sub_solves'] = 10
        newton.options['reraise_child_analysiserror'] = False
        newton.linesearch = om.BoundsEnforceLS()
        self.linear_solver = om.DirectSolver()

        # base_class setup should be called as the last thing in your setup
        super().setup()

class MPpropulsor(pyc.MPCycle):

    def setup(self):

        design = self.pyc_add_pnt('design', Propulsor(design=True, thermo_method='CEA'))
        self.pyc_add_cycle_param('pwr_target', 100.)

        # define the off-design conditions we want to run
        self.od_pts = ['off_design']
        self.od_MNs = [0.8,]
        self.od_alts = [15000,]
        self.od_Rlines = [2.2,]

        for i, pt in enumerate(self.od_pts):
            self.pyc_add_pnt(pt, Propulsor(design=False, thermo_method='CEA'))

            self.set_input_defaults(pt+'.fc.MN', val=self.od_MNs[i])
            self.set_input_defaults(pt+'.fc.alt', val=self.od_alts, units='m') 
            self.set_input_defaults(pt+'.fan.map.RlineMap', val=self.od_Rlines[i])        

        self.pyc_use_default_des_od_conns()

        self.pyc_connect_des_od('nozz.Throat:stat:area', 'balance.rhs:W')

        super().setup()

Next, let’s run the propulser through a design and off-design case to see what happens.

import numpy as np

prob = om.Problem()

prob.model = mp_propulsor = MPpropulsor()

prob.setup()

#Define the design point
prob.set_val('design.fc.alt', 10000, units='m')
prob.set_val('design.fc.MN', 0.8)
prob.set_val('design.inlet.MN', 0.6)
prob.set_val('design.fan.PR', 1.2)
prob.set_val('pwr_target', -3486.657, units='hp')
prob.set_val('design.fan.eff', 0.96)

# Set initial guesses for balances
prob['design.balance.W'] = 500.

for i, pt in enumerate(mp_propulsor.od_pts):
    # initial guesses
    prob['off_design.fan.PR'] = 3.
    prob['off_design.balance.W'] = 10.
    prob['off_design.balance.Nmech'] = 20. # normalized value

prob.set_solver_print(level=-1)
prob.set_solver_print(level=2, depth=2)
prob.model.design.nonlinear_solver.options['atol'] = 1e-6
prob.model.design.nonlinear_solver.options['rtol'] = 1e-6

prob.model.off_design.nonlinear_solver.options['atol'] = 1e-6
prob.model.off_design.nonlinear_solver.options['rtol'] = 1e-6


prob.run_model()
======
design
======
NL: Newton 0 ; 0.468905936 1
|  LS: BCHK 0 ; 50.7119874 108.149596
NL: Newton 1 ; 0.0900132352 0.191964376
|  LS: BCHK 0 ; 0.00611530407 0.0679378322
NL: Newton 2 ; 0.00611400388 0.0130388707
|  LS: BCHK 0 ; 3.68697569e-05 0.00603037839
NL: Newton 3 ; 3.6869756e-05 7.86293225e-05
|  LS: BCHK 0 ; 1.35988258e-09 3.68834169e-05
NL: Newton 4 ; 1.35927833e-09 2.89882943e-09
NL: Newton Converged

==========
off_design
==========
NL: Newton 0 ; 16.13985 1
|  LS: BCHK 0 ; 98264889.5 6088339.71
NL: Newton 1 ; 2.66730484 0.165262059
|  LS: BCHK 0 ; 3.21833742e+10 1.20658778e+10
NL: Newton 2 ; 64.0614724 3.96914918
|  LS: BCHK 0 ; 3.48694839e+10 544312870
off_design.nozz.staticMN.ps_resid [-2189344.43307203] [7411947.00498394] [150.]
off_design.nozz.staticMN.ps_resid [-2189344.43307203] [7411947.00498394] [150.]
off_design.nozz.staticMN.ps_resid [-4.81875566] [287.05016442] [187.73045403]
off_design.nozz.staticMN.ps_resid [-4.81875566] [287.05016442] [187.73045403]
/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/solver.py:651: SolverWarning:Your model has stalled three times and may be violating the bounds. In the future, turn on print_bound_enforce in your solver options here: 
off_design.fan.ideal_flow.nonlinear_solver.linesearch.options['print_bound_enforce']=True. 
The bound(s) being violated now are:

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/linesearch/backtracking.py:39: SolverWarning:'off_design.fan.ideal_flow.balance.T' exceeds lower bounds
  Val: [-2.44706198e+11]
  Lower: [150.]

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/solver.py:651: SolverWarning:Your model has stalled three times and may be violating the bounds. In the future, turn on print_bound_enforce in your solver options here: 
off_design.fan.real_flow.nonlinear_solver.linesearch.options['print_bound_enforce']=True. 
The bound(s) being violated now are:

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/linesearch/backtracking.py:35: SolverWarning:'off_design.fan.real_flow.balance.T' exceeds upper bounds
  Val: [2613.1103812]
  Upper: [2500.]

/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/elements/compressor.py:75: RuntimeWarning: invalid value encountered in log
  outputs['eff_poly'] = Rt * np.log(PR) / ( Rt*np.log(PR) + S_out - S_in )
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/solver.py:651: SolverWarning:Your model has stalled three times and may be violating the bounds. In the future, turn on print_bound_enforce in your solver options here: 
off_design.nozz.throat_total.nonlinear_solver.linesearch.options['print_bound_enforce']=True. 
The bound(s) being violated now are:

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/linesearch/backtracking.py:35: SolverWarning:'off_design.nozz.throat_total.balance.T' exceeds upper bounds
  Val: [2613.1103812]
  Upper: [2500.]

/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:108: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:108: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5
/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/solver.py:651: SolverWarning:Your model has stalled three times and may be violating the bounds. In the future, turn on print_bound_enforce in your solver options here: 
off_design.nozz.staticMN.nonlinear_solver.linesearch.options['print_bound_enforce']=True. 
The bound(s) being violated now are:

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/linesearch/backtracking.py:39: SolverWarning:'off_design.nozz.staticMN.ps_resid.Ps' exceeds lower bounds
  Val: [-772646.27279157]
  Lower: [-0.0009009]

/home/john/anaconda3/envs/course/lib/python3.8/site-packages/openmdao/solvers/linesearch/backtracking.py:39: SolverWarning:'off_design.nozz.staticMN.ps_resid.area' exceeds lower bounds
  Val: [-1.19971121e-09]
  Lower: [1.e-05]
NL: Newton 3 ; nan nan
NL: NewtonSolver 'NL: Newton' on system 'off_design': residuals contain 'inf' or 'NaN' after 3 iterations.
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/elements/compressor.py:75: RuntimeWarning: invalid value encountered in log
  outputs['eff_poly'] = Rt * np.log(PR) / ( Rt*np.log(PR) + S_out - S_in )
/mnt/c/Users/John/Dropbox/git/pyCycle/pycycle/thermo/static_ps_resid.py:128: RuntimeWarning: invalid value encountered in sqrt
  Vsonic = (i['gamma']*i['R']*i['Ts'])**0.5

The design case converges well, but the off-design Newton solver fails after a few iterations. We get a whole bunch of warnings and some hints about what could be going on. How can you get this solver to converge?