Warm start on the IDAES CSTR#

Under closed-loop control the next problem is the last one moved one step, so the last solution moved one step is nearly its answer. drto.warm_start_dynamic shifts it. This notebook runs one loop iteration on the pattern a loop actually uses: one persistent model, built and scaled once, then solved, shifted, and solved again, the second solve told only that the barrier may start small. The shift carries values only; the solver rebuilds its own multipliers from a good starting point in its first iterations, faster than any multipliers we could hand it. The solver is pounce, which reads the scaling suffix and the option through its standard interface.

One model, built and scaled once#

Declarations, the setpoint, the terminal segment, the cold start with the flowsheet’s magnitudes as its scale source, energy in joules near 1e7 and duties in watts near 1e6, then the assembly and drto.scale writing the same magnitudes for the solves. The whole loop keeps this one model, and every solve receives the factors under nlp_scaling_method=user-scaling.

import contextlib, io, time

import pyomo.environ as pyo
from pyomo.contrib.solver.common.factory import SolverFactory

import drto
from models.idaes_cstr import DC_START, F_IN, VOLUME, build

# the horizon, the terminal segment, and the assembly in one call. The
# builder holds the feed at the declared sample points and this fixes the
# members collocation adds to it
m = drto.dynamic_optimization(build, N=10, infinite_horizon=True)

# the setpoint comes off a steady simulation, which reduces a declared
# model, so it runs on a second build rather than on the assembled one
declared = build()
ss = pyo.TransformationFactory("drto.steady_state_simulation").create_using(
    declared,
    controls={declared.fs.cstr.control_volume.heat.name: 0.0,
              declared.fs.cstr.inlet.flow_vol.name: F_IN})
drto.initialize_steady_state(ss)
drto.scaled_solve(ss)
cvs = ss.fs.cstr.control_volume
for j, ssp in (("NaOH", m.ss_naoh), ("EthylAcetate", m.ss_ea),
               ("SodiumAcetate", m.ss_sa), ("Ethanol", m.ss_etoh)):
    ssp.set_value(pyo.value(cvs.material_holdup["Liq", j]))
for j, sgn in (("NaOH", 1), ("EthylAcetate", 1),
               ("SodiumAcetate", -1), ("Ethanol", -1)):
    m.mat0[j] = pyo.value(cvs.material_holdup["Liq", j]) + sgn * DC_START * VOLUME
for k in m.eng_ss:
    m.eng_ss[k] = pyo.value(cvs.energy_holdup[k])

drto.cold_start_dynamic(m, profile="exponential", time_constant=3.0,
                        scale={"J": 1e7, "W": 1e6})

drto.scale(m, source={"J": 1e7, "W": 1e6})
res = SolverFactory("pounce").solve(
    m,
    solver_options={"nlp_scaling_method": "user-scaling", "mu_init": 1e-6},
    tee=True)
print(res.termination_condition.name)
********************************************************************************

                    ####    ###   /   # /#   #/  ####  #####
                    #   #  #   # /#   #/ ##  /  #      #
                    ####   #   #/ #   /  # #/#  #      ####
                    #      #   /  #  /#  # /##  #      #
                    #       ##/    #/#   #/  #   ####  #####

********************************************************************************
This program contains POUNCE, a pure-Rust interior-point optimization solver
for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).
Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.
         For more information visit https://github.com/jkitchin/pounce
********************************************************************************

This is POUNCE version 0.11.0, running with linear solver FERAL.

Number of nonzeros in equality constraint Jacobian...:     4461
Number of nonzeros in inequality constraint Jacobian.:       20
Number of nonzeros in Lagrangian Hessian.............:      645

Total number of variables............................:     1408
                     variables with only lower bounds:      342
                variables with lower and upper bounds:       92
                     variables with only upper bounds:        0
Total number of equality constraints.................:     1338
Total number of inequality constraints...............:        4
        inequality constraints with only lower bounds:        4
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
   0  6.7507105e+03 1.59e+07 1.02e+03   -6.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1 -1.5023966e+03 5.55e+07 9.98e+02   -6.0 2.07e+05      - 1.51e-02 2.06e-02f  1
   2 -2.4350856e+03 5.54e+07 9.93e+02   -6.0 4.93e+05      - 5.53e-03 1.72e-03f  1
   3 -1.5974479e+04 7.39e+07 9.89e+02   -6.0 7.27e+05      - 3.63e-03 1.73e-02f  1
   4 -1.8633126e+04 7.41e+07 9.82e+02   -6.0 7.46e+04      - 7.62e-03 1.86e-02f  1
   5 -2.2609629e+04 6.96e+07 9.69e+02   -6.0 6.61e+04      - 1.32e-02 4.39e-02f  1
   6 -2.1404378e+04 6.58e+07 5.42e+02   -6.0 1.50e+04      - 4.41e-01 5.38e-02h  1
   7  2.2012740e+03 2.02e+07 5.82e+02   -6.0 1.47e+04      - 6.37e-01 1.00e+00h  1
   8  2.7583378e+03 1.05e+07 8.54e+01   -6.0 9.07e+02      - 6.89e-01 1.00e+00h  1
   9  3.4686896e+03 1.69e+06 1.31e+01   -6.0 3.35e+02      - 7.85e-01 1.00e+00h  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  10  3.5362653e+03 2.70e+04 1.41e-01   -6.0 3.57e+01      - 9.89e-01 1.00e+00h  1
  11  3.5376312e+03 8.54e-01 4.11e-04   -6.0 1.01e+00      - 1.00e+00 1.00e+00h  1
  12  3.5376313e+03 6.93e-07 3.77e-10   -6.0 4.94e-05      - 1.00e+00 1.00e+00h  1
  13  3.5376313e+03 2.90e-07 1.89e-11   -9.0 1.00e-04      - 1.00e+00 1.00e+00h  1


Number of Iterations....: 13

                                   (scaled)                 (unscaled)
Objective...............:   3.5376313199799724e+03    3.5376313199799724e+03
Dual infeasibility......:   1.8873441754458742e-11    1.8873441754458742e-11
Constraint violation....:   1.1273837766112571e-12    2.8958146458535339e-07
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   1.0061417873177911e-09    1.0061417873177911e-09
Overall NLP error.......:   1.0061417873177911e-09    2.8958146458535339e-07


Number of objective function evaluations             = 14
Number of objective gradient evaluations             = 14
Number of equality constraint evaluations            = 14
Number of inequality constraint evaluations          = 14
Number of equality constraint Jacobian evaluations   = 14
Number of inequality constraint Jacobian evaluations = 14
Number of Lagrangian Hessian evaluations             = 14
Number of linear solver quality escalations          = 2
Total seconds in POUNCE                              = 0.194

EXIT: Optimal Solution Found.

POUNCE 0.11.0: Optimal Solution Found.
convergenceCriteriaSatisfied

One step later: shift everything#

The loop implements the first move and the state advances one sample; the model’s own solution at t = h stands in for the measurement, read directly, since the model never leaves its own units. The shift moves every variable one sampling time forward.

cv = m.fs.cstr.control_volume
h = 1.0
for j in ("NaOH", "EthylAcetate", "SodiumAcetate", "Ethanol"):
    m.mat0[j] = pyo.value(cv.material_holdup[h, "Liq", j])
m.eng0["Liq"] = pyo.value(cv.energy_holdup[h, "Liq"])

print(drto.warm_start_dynamic(m))
drto warm_start_dynamic (the previous solution, one step on)
  shift         : 1 time units
  copied        : 1009 values on aligned points
  interpolated  : 496 values between points
  filled        : 0 values past the end
  tail          : shifted through t = tN + atanh(tau)/gamma

The second solve, warm#

The same model again from the shifted values, with the same scaling and mu_init=1e-6: the shifted point is nearly optimal, so the barrier starts where it would otherwise have to work its way down to, and the solver rebuilds its multipliers from the point and stops.

res = SolverFactory("pounce").solve(
    m,
    solver_options={"nlp_scaling_method": "user-scaling", "mu_init": 1e-6},
    tee=True)
print(res.termination_condition.name)
********************************************************************************

                    ####    ###   /   # /#   #/  ####  #####
                    #   #  #   # /#   #/ ##  /  #      #
                    ####   #   #/ #   /  # #/#  #      ####
                    #      #   /  #  /#  # /##  #      #
                    #       ##/    #/#   #/  #   ####  #####

********************************************************************************
This program contains POUNCE, a pure-Rust interior-point optimization solver
for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).
Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.
         For more information visit https://github.com/jkitchin/pounce
********************************************************************************

This is POUNCE version 0.11.0, running with linear solver FERAL.

Number of nonzeros in equality constraint Jacobian...:     4461
Number of nonzeros in inequality constraint Jacobian.:       20
Number of nonzeros in Lagrangian Hessian.............:      645

Total number of variables............................:     1408
                     variables with only lower bounds:      342
                variables with lower and upper bounds:       92
                     variables with only upper bounds:        0
Total number of equality constraints.................:     1338
Total number of inequality constraints...............:        4
        inequality constraints with only lower bounds:        4
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
   0  1.6659160e+02 1.71e+07 1.02e+03   -6.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1  6.5532589e+01 1.68e+07 9.61e+02   -6.0 6.95e+01      - 8.04e-01 2.06e-02f  1
   2  6.5098541e+01 1.61e+07 2.40e+02   -6.0 7.41e+01      - 7.50e-01 4.36e-02h  1
   3  6.3346397e+01 5.85e+05 4.03e+01   -6.0 5.11e+01      - 8.33e-01 1.00e+00h  1
   4  4.6242123e+01 2.57e+05 5.09e+00   -6.0 1.95e+01      - 8.74e-01 1.00e+00f  1
   5  6.5584014e+01 1.03e+02 5.92e-03   -6.0 1.28e+01      - 9.99e-01 1.00e+00h  1
   6  6.5591612e+01 1.10e-04 6.69e-09   -6.0 4.70e-03      - 1.00e+00 1.00e+00h  1
   7  6.5591602e+01 2.98e-07 1.63e-11   -9.0 1.00e-04      - 1.00e+00 1.00e+00h  1


Number of Iterations....: 7

                                   (scaled)                 (unscaled)
Objective...............:   6.5591601659451740e+01    6.5591601659451740e+01
Dual infeasibility......:   1.6303947340023779e-11    1.6303947340023779e-11
Constraint violation....:   1.7712346563617792e-12    2.9821643465766101e-07
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   1.0053054707570988e-09    1.0053054707570988e-09
Overall NLP error.......:   1.0053054707570988e-09    2.9821643465766101e-07


Number of objective function evaluations             = 8
Number of objective gradient evaluations             = 8
Number of equality constraint evaluations            = 8
Number of inequality constraint evaluations          = 8
Number of equality constraint Jacobian evaluations   = 8
Number of inequality constraint Jacobian evaluations = 8
Number of Lagrangian Hessian evaluations             = 8
Number of linear solver quality escalations          = 3
Total seconds in POUNCE                              = 0.073

EXIT: Optimal Solution Found.

POUNCE 0.11.0: Optimal Solution Found.
convergenceCriteriaSatisfied

The cold solve above took thirteen iterations and the warm-started one takes seven: the shifted solution is nearly the answer, and the solve rebuilds its multipliers from it and stops. That is warm starting a receding horizon, whole.