An IDAES flowsheet under drto#

Every other example here declares its model by hand. This one takes a unit model straight out of IDAES and drives it with drto, to show that the declaration surface attaches to a flowsheet somebody else built.

The plant is a CSTR running the saponification of ethyl acetate, NaOH + EthylAcetate -> SodiumAcetate + Ethanol, from idaes.models.unit_models. The reaction is exothermic, the manipulated variables are the jacket duty and the feed flow, as in the hicks example, and the controlled variable is reactor temperature. The reactor starts hot, at 318 K, and short of conversion, and the controller has to bring it to the 304.04 K steady state the nominal feed alone would settle at.

The whole example is two solves on one declared flowsheet: the model collapsed to its setpoint, and the infinite-horizon controller.

Residence time is V/F = 1 s at the nominal flow, so the horizon is ten seconds at one-second samples: ten residence times on the horizon, with the infinite tail carrying the rest.

%matplotlib inline
import pyomo.environ as pyo
import matplotlib.pyplot as plt
import drto
from drto import plot_controls, plot_stage_cost, plot_states
from idaes.core.util.model_statistics import degrees_of_freedom
from models.idaes_cstr import DC_START, F_IN, VOLUME, build

Declaring the flowsheet#

drto.horizon captures the set’s points as the sample grid, so it has to be called before discretization. That means the sample times go into FlowsheetBlock(time_set=...) up front rather than being introduced by the discretizer.

The rest maps one-to-one onto what IDAES already built. The states are the holdups, whose DerivativeVars are the accumulation terms; the dynamics are the material and enthalpy balances; the controls are the jacket duty and the feed flow. The duty is declared on control_volume.heat and not on the unit-level heat_duty, which is a Pyomo Reference to it: drto classifies on the referent, and declaring the Reference leaves heat looking like an unexplained algebraic variable. The flow has no flat Var at all, its variables living inside the property-block members, so it is declared on the inlet Port’s time-indexed Reference, the IDAES inlet idiom. After the transforms, drto.info(m).components("control") hands back the live control components either way.

The targets are mutable Params, declared empty and filled from the steady solve below: the declarations come first, and the numbers flow in afterward.

The starting point takes no solve, and it pins the true states only. The reactor is constant volume with constant molar density, so the water holdup is algebraic (the package fixes its concentration) and carries no initial condition: the initial condition covers the four reacting species and the energy. The reactor starts hot and short of conversion: the setpoint composition knocked back along the reaction by DC_START, at 318 K, and the controller has to bring both back.

declared = build()
drto.info(declared)
drto registry
states: 5, controls: 12
horizonfs._time (ContinuousSet, 11 points)
statesfs.cstr.control_volume.material_holdup_Liq_NaOH (free, mol), fs.cstr.control_volume.material_holdup_Liq_EthylAcetate (free, mol), fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate (free, mol), fs.cstr.control_volume.material_holdup_Liq_Ethanol (free, mol), fs.cstr.control_volume.energy_holdup (free, J)
dynamicsfs.cstr.control_volume.material_accumulation[t,p,j] == fs.cstr.control_volume.properties_in[t].flow_vol*fs.cstr.control_volume.properties_in[t].conc_mol_comp[j] - fs.cstr.control_volume.properties_out[t].flow_vol*fs.cstr.control_volume.properties_out[t].conc_mol_comp[j] + fs.cstr.control_volume.rate_reaction_generation[t,p,j] for t in fs.cstr.control_volume.SetProduct_OrderedSet (mol/s)
dynamicsfs.cstr.control_volume.energy_accumulation[t,Liq] == fs.props.dens_mol*fs.props.cp_mol*fs.cstr.control_volume.properties_in[t].flow_vol*(fs.cstr.control_volume.properties_in[t].temperature - fs.props.temperature_ref) - fs.props.dens_mol*fs.props.cp_mol*fs.cstr.control_volume.properties_out[t].flow_vol*(fs.cstr.control_volume.properties_out[t].temperature - fs.props.temperature_ref) + fs.cstr.control_volume.heat[t] + (- fs.rxn.dh_rxn[R1]*fs.cstr.control_volume.rate_reaction_extent[t,R1]) for t in fs._time (W)
controlsfs.cstr.control_volume.heat (piecewise_constant, free, W), fs.cstr._flow_vol_inlet_ref (piecewise_constant, free, m**3/s)
tracking stage costcost[t] == ((fs.cstr.control_volume.energy_holdup[t,'Liq'] - eng_ss[Liq])/scale_E)**2 + ((fs.cstr.control_volume.heat[t] - duty_ss)/scale_Q)**2 + ((fs.cstr._flow_vol_inlet_ref[t] - flow_ss)/scale_F)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','NaOH'] - ss_naoh)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','EthylAcetate'] - ss_ea)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','SodiumAcetate'] - ss_sa)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','Ethanol'] - ss_etoh)/scale_M)**2 for t in sorted(fs._time)[:-1]
terminal costterm == ((fs.cstr.control_volume.material_holdup[10.0,Liq,NaOH] - ss_naoh)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,EthylAcetate] - ss_ea)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,SodiumAcetate] - ss_sa)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,Ethanol] - ss_etoh)/scale_M)**2 + ((fs.cstr.control_volume.energy_holdup[10.0,Liq] - eng_ss[Liq])/scale_E)**2
initial conditionsmat0[j] == fs.cstr.control_volume.material_holdup[0.0,'Liq',j] for j in OrderedScalarSet (mol)
initial conditionseng0[p] == fs.cstr.control_volume.energy_holdup[0.0,p] for p in fs.props.phase_list (J)
steady-state targetsss_naoh (of fs.cstr.control_volume.material_holdup_Liq_NaOH, mol), ss_ea (of fs.cstr.control_volume.material_holdup_Liq_EthylAcetate, mol), ss_sa (of fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate, mol), ss_etoh (of fs.cstr.control_volume.material_holdup_Liq_Ethanol, mol), eng_ss (of fs.cstr.control_volume.energy_holdup, J)
steady-state control targetsduty_ss (of fs.cstr.control_volume.heat, W), flow_ss (of fs.cstr._flow_vol_inlet_ref, m**3/s)
transformations (none)

Scaling#

The flowsheet runs in raw SI: energy holdups at 1e7 J and duties at 1e6 W against unit flows, and the collocation equations carry those magnitudes with order-one coefficients, so double precision floors the residuals near 1e-7 and the solver stops at an acceptable level instead of full tolerance. drto.scale measures a factor per variable group from the magnitudes the model holds, and a factor per large constraint from its Jacobian at those values, so it runs after an initializer has filled the model. drto.scaled_solve assigns the factors and solves: the suffix reaches pounce under nlp_scaling_method=user-scaling, and the solution comes back in the model’s own units. Both solves below run through it, each after its initializer.

The setpoint from the declarations#

The setpoint is where the nominal feed alone drives the reactor, and the declared model already contains that equilibrium. drto.steady_state_simulation collapses the flowsheet to a single point and fixes the controls, the duty at zero and the flow at its nominal value: time leaves the model, the accumulation terms are pinned, and the IDAES property blocks collapse to their steady member alongside the flat holdups. The steady target comes from the same physics the controller uses, with no second flowsheet to build or keep in sync.

The solution fills the mutable Params: the tracking targets, and the base composition the initial condition perturbs. The controller below is built by its own call, so the numbers are collected here and written onto it there.

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})
assert degrees_of_freedom(ss) == 0
print(drto.initialize_steady_state(ss))
res = drto.scaled_solve(ss, tee=True)

cvs = ss.fs.cstr.control_volume
print(f"setpoint  T = {pyo.value(cvs.properties_out[0.0].temperature):7.3f} K"
      f"   NaOH = {pyo.value(cvs.properties_out[0.0].conc_mol_comp['NaOH']):6.3f} mol/m3")

# the controller below is its own build, so the numbers are collected here
# and written onto it rather than onto this model
target = {name: pyo.value(cvs.material_holdup["Liq", j])
          for j, name in (("NaOH", "ss_naoh"), ("EthylAcetate", "ss_ea"),
                          ("SodiumAcetate", "ss_sa"), ("Ethanol", "ss_etoh"))}
# the start: the setpoint composition with conversion knocked back by
# DC_START along the reaction stoichiometry, hot by T_START - T_ss
start = {j: pyo.value(cvs.material_holdup["Liq", j]) + sgn * DC_START * VOLUME
         for j, sgn in (("NaOH", 1), ("EthylAcetate", 1),
                        ("SodiumAcetate", -1), ("Ethanol", -1))}
energy = {k: pyo.value(cvs.energy_holdup[k]) for k in declared.eng_ss}
drto.info(ss)
pyomo-pounce initialize (fill -> repair -> block-solve)
  decisions held: 0
  values filled : 0
  projection    : optimal
  pyomo-pounce block_initialize
    decisions fixed   : 0
    system square     : True
    blocks solved     : 15 (14 by Newton 1x1, 1 subsystem solves)
    vars initialized  : 22
    left untouched    : 0 underdetermined, 0 overdetermined
********************************************************************************

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

********************************************************************************
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.

Reading C:\Users\Devin\AppData\Local\Temp\tmp9yl5h6g0\unknown0hr0vd13.nl...
Parsed 8 vars, 8 cons, jac_nnz=22, h_nnz=9 in 0.00s
Problem class: NLP. Selected solver: NLP filter line-search interior-point (pounce-nlp) [solver_selection=auto].

Number of nonzeros in equality constraint Jacobian...:       22
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:        9

Total number of variables............................:        8
                     variables with only lower bounds:        5
                variables with lower and upper bounds:        1
                     variables with only upper bounds:        0
Total number of equality constraints.................:        8
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   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  0.0000000e+00 2.60e-16 1.00e+00   -1.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1  0.0000000e+00 2.08e-17 0.00e+00   -1.0 3.64e-14      - 9.91e-01 1.00e+00h  1


Number of Iterations....: 1

                                   (scaled)                 (unscaled)
Objective...............:   0.0000000000000000e+00    0.0000000000000000e+00
Dual infeasibility......:   0.0000000000000000e+00    0.0000000000000000e+00
Constraint violation....:   2.0816681711721685e-17    2.0816681711721685e-17
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   2.0816681711721685e-17    2.0816681711721685e-17
  ...above the per-row floating-point noise floor:   0.0000000000000000e+00
  (the strict convergence test judges this value; the residual below it is finer than the row's own arithmetic can resolve. Set primal_noise_floor_kappa = 0 to disable.)


Number of objective function evaluations             = 2
Number of objective gradient evaluations             = 1
Number of equality constraint evaluations            = 2
Number of inequality constraint evaluations          = 2
Number of equality constraint Jacobian evaluations   = 2
Number of inequality constraint Jacobian evaluations = 2
Number of Lagrangian Hessian evaluations             = 2
Total seconds in POUNCE                              = 0.002

EXIT: Optimal Solution Found.

POUNCE 0.11.0: Optimal Solution Found.
Status: Solve_Succeeded
pounce: wrote C:\Users\Devin\AppData\Local\Temp\tmp9yl5h6g0\unknown0hr0vd13.sol
setpoint  T = 304.039 K   NaOH = 24.286 mol/m3
drto registry
states: 5, controls: 2
statesfs.cstr.control_volume.material_holdup_Liq_NaOH (free), fs.cstr.control_volume.material_holdup_Liq_EthylAcetate (free), fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate (free), fs.cstr.control_volume.material_holdup_Liq_Ethanol (free), fs.cstr.control_volume.energy_holdup (free)
dynamicsfs.cstr.control_volume.material_accumulation[o,_r] == fs.cstr.control_volume.properties_in[0.0].flow_vol*fs.cstr.control_volume.properties_in[0.0].conc_mol_comp[_r] - fs.cstr.control_volume.properties_out[0.0].flow_vol*fs.cstr.control_volume.properties_out[0.0].conc_mol_comp[_r] + fs.cstr.control_volume.rate_reaction_generation[o,_r] for o in fs.props._phase_component_set (inc)
dynamicsfs.cstr.control_volume.energy_accumulation[Liq] == fs.props.dens_mol*fs.props.cp_mol*fs.cstr.control_volume.properties_in[0.0].flow_vol*(fs.cstr.control_volume.properties_in[0.0].temperature - fs.props.temperature_ref) - fs.props.dens_mol*fs.props.cp_mol*fs.cstr.control_volume.properties_out[0.0].flow_vol*(fs.cstr.control_volume.properties_out[0.0].temperature - fs.props.temperature_ref) + fs.cstr.control_volume.heat - fs.rxn.dh_rxn[R1]*fs.cstr.control_volume.rate_reaction_extent[R1] (inc)
controlsfs.cstr.control_volume.heat (fixed), fs.cstr._flow_vol_inlet_ref (fixed, m**3/s)
steady-state targetsss_naoh (of fs.cstr.control_volume.material_holdup_Liq_NaOH, mol), ss_ea (of fs.cstr.control_volume.material_holdup_Liq_EthylAcetate, mol), ss_sa (of fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate, mol), ss_etoh (of fs.cstr.control_volume.material_holdup_Liq_Ethanol, mol), eng_ss (of fs.cstr.control_volume.energy_holdup, J)
steady-state control targetsduty_ss (of fs.cstr.control_volume.heat, W), flow_ss (of fs.cstr._flow_vol_inlet_ref, m**3/s)
transformations
  1. drto.dynamic_to_steady_state: removed=initial condition, tracking terminal cost, collapsed=8 Vars and 8 Constraints to a single point, derivatives=2 fixed at zero, blocks=3 time-indexed Block(s) collapsed to the steady member, 30 member(s) removed
  2. drto.build_objective: objective=zero
  3. drto.steady_state_simulation: controls=fs.cstr.control_volume.heat=0.0, fs.cstr._flow_vol_inlet_ref=1.0, dropped=tracking stage cost

The infinite-horizon controller#

drto.dynamic_optimization takes the builder rather than a model and reaches the controller in one call: it discretizes the horizon at one finite element per declared sample, applies drto.infinite_horizon with the options given, and assembles the objective. The builder fixes the feed at the declared sample points, which is all an undiscretized model has, and the call fixes the members collocation adds to it.

The terminal segment replicates the IDAES property blocks onto the tail as algebraic families, and the feed conditions carry onto it with no declaration involved. The concentrations, temperature, and pressure are fixed variables, and fixed means fixed: each one’s segment copy is fixed at its horizon-end value. (They are not disturbances: drto’s disturbances are zero-mean noise, which the optimization mode fixes at zero on the horizon.)

drto.cold_start_dynamic then fills the model, interpolating each state from its declared initial condition to its target, solving the algebra at every point, and placing the segment’s copies at the targets.

# the horizon discretized, the terminal segment appended, and the objective
# assembled, from the builder in one call
m = drto.dynamic_optimization(
    build, N=10,
    infinite_horizon={
        "nfe": 1,                  # finite elements on the terminal segment
        "ncp": 5,                  # Gauss-Legendre points per element
        "beta": 1.2,               # tail overestimation factor, > 1
        "gamma": "rule",           # the mesh rule, tanh(gamma*dt) = tau_11;
                                   # pass a number to override
        "profile": "collocation",  # segment control profile (pyomo-cvp)
    },
)
for name, value in target.items():
    m.component(name).set_value(value)
for j, value in start.items():
    m.mat0[j] = value
for k, value in energy.items():
    m.eng_ss[k] = value

print(drto.cold_start_dynamic(m, scale={"J": 1e7, "W": 1e6}))
print("degrees of freedom:", degrees_of_freedom(m))
drto cold_start_dynamic (interpolate to the targets)
  states        : 5 on a line across 31 grid points
  derivatives   : 155 at the profile's slope
  controls      : 2 at their targets
  segment       : 12 copies at the targets, slacks at zero
  point solves  : run (pyomo-pounce block solve)
  pyomo-pounce initialize (fill -> repair -> block-solve)
    decisions held: 220
    values filled : 0
    projection    : skipped
    pyomo-pounce block_initialize
      decisions fixed   : 0
      system square     : True
      blocks solved     : 822 (822 by Newton 1x1, 0 subsystem solves)
      vars initialized  : 822
      left untouched    : 0 underdetermined, 0 overdetermined
degrees of freedom: 40
res = drto.scaled_solve(m, tee=True)
drto.info(m)
********************************************************************************

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

********************************************************************************
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...:     3441
Number of nonzeros in inequality constraint Jacobian.:       10
Number of nonzeros in Lagrangian Hessian.............:      475

Total number of variables............................:     1098
                     variables with only lower bounds:      272
                variables with lower and upper bounds:       72
                     variables with only upper bounds:        0
Total number of equality constraints.................:     1058
Total number of inequality constraints...............:        2
        inequality constraints with only lower bounds:        2
   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.3163721e+04 3.01e+07 1.00e+04   -1.0 0.00e+00      - 0.00e+00 0.00e+00   0
   1 -6.9391494e+03 5.61e+07 9.96e+03   -1.0 1.98e+10      - 3.00e-03 4.30e-03f  1
   2 -5.0950532e+04 1.02e+08 1.17e+04   -1.0 6.96e+09      - 8.91e-05 1.65e-02f  1
   3 -5.9040836e+04 1.02e+08 1.17e+04   -1.0 9.76e+09      - 1.39e-03 2.65e-03f  1
   4 -7.2753734e+04 1.02e+08 1.12e+04   -1.0 1.83e+09      - 1.56e-02 1.74e-02f  1
   5 -7.3881857e+04 9.97e+07 1.09e+04   -1.0 2.25e+08      - 8.93e-02 2.35e-02f  1
   6 -3.8888338e+04 7.47e+07 8.89e+03   -1.0 1.05e+08      - 1.27e-01 4.65e-01H  1
   7 -4.7745315e+04 4.42e+07 9.93e+03   -1.0 9.38e+07      - 4.11e-01 2.74e-01f  1
   8 -1.6376394e+04 1.70e+07 1.24e+04   -1.0 2.68e+07      - 5.76e-01 6.90e-01h  1
   9  1.9996379e+03 2.35e+07 1.47e+03   -1.0 3.33e+06      - 7.54e-01 1.00e+00h  1
iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls
  10  3.3362839e+03 4.39e+06 1.21e+03   -1.0 4.91e+06      - 9.81e-01 1.00e+00h  1
  11  3.5148340e+03 2.24e+05 3.01e+02   -1.0 2.39e+06      - 8.21e-01 1.00e+00h  1
  12  3.5384622e+03 2.36e+03 1.04e+00   -1.0 2.11e+05      - 1.00e+00 1.00e+00h  1
  13  3.5386478e+03 3.64e-01 8.70e-05   -1.0 4.76e+02      - 1.00e+00 1.00e+00h  1
  14  3.5376450e+03 2.87e+01 8.41e-02   -2.5 9.32e+03      - 1.00e+00 1.00e+00f  1
  15  3.5376596e+03 3.53e-04 1.38e-06   -2.5 4.25e+01      - 1.00e+00 1.00e+00h  1
  16  3.5376328e+03 2.20e-02 6.50e-05   -3.8 2.59e+02      - 1.00e+00 1.00e+00f  1
  17  3.5376313e+03 1.65e-03 2.01e-07   -5.7 1.25e+05      - 1.00e+00 1.00e+00f  1
  18  3.5376313e+03 1.81e-04 2.60e-01   -8.6 1.32e+05      - 1.00e+00 9.85e-01h  1
  19  3.5376313e+03 3.65e-07 7.98e-12   -8.6 5.30e+03      - 1.00e+00 1.00e+00f  1


Number of Iterations....: 19

                                   (scaled)                 (unscaled)
Objective...............:   3.5376313199960605e+03    3.5376313199960605e+03
Dual infeasibility......:   7.9849529983875834e-12    7.9849529983875834e-12
Constraint violation....:   1.6980687689294172e-13    3.6507844924926753e-07
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.5059642828841207e-09    2.5059642828841207e-09
Overall NLP error.......:   2.5059642828841207e-09    3.6507844924926753e-07


Number of objective function evaluations             = 21
Number of objective gradient evaluations             = 20
Number of equality constraint evaluations            = 21
Number of inequality constraint evaluations          = 21
Number of equality constraint Jacobian evaluations   = 20
Number of inequality constraint Jacobian evaluations = 20
Number of Lagrangian Hessian evaluations             = 20
Number of linear solver quality escalations          = 1
Total seconds in POUNCE                              = 0.069

EXIT: Optimal Solution Found.

POUNCE 0.11.0: Optimal Solution Found.
drto registry
states: 5, controls: 2
horizonfs._time (ContinuousSet, 31 points)
statesfs.cstr.control_volume.material_holdup_Liq_NaOH (free, mol), fs.cstr.control_volume.material_holdup_Liq_EthylAcetate (free, mol), fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate (free, mol), fs.cstr.control_volume.material_holdup_Liq_Ethanol (free, mol), fs.cstr.control_volume.energy_holdup (free, J)
dynamicsfs.cstr.control_volume.material_accumulation[t,p,j] == fs.cstr._flow_vol_inlet_ref[0.0]*fs.cstr.control_volume.properties_in[t].conc_mol_comp[j] - fs.cstr.control_volume.properties_out[t].flow_vol*fs.cstr.control_volume.properties_out[t].conc_mol_comp[j] + fs.cstr.control_volume.rate_reaction_generation[t,p,j] for t in fs.cstr.control_volume.SetProduct_OrderedSet (mol/s)
dynamicsfs.cstr.control_volume.energy_accumulation[t,Liq] == fs.props.dens_mol*fs.props.cp_mol*fs.cstr._flow_vol_inlet_ref[0.0]*(fs.cstr.control_volume.properties_in[t].temperature - fs.props.temperature_ref) - fs.props.dens_mol*fs.props.cp_mol*fs.cstr.control_volume.properties_out[t].flow_vol*(fs.cstr.control_volume.properties_out[t].temperature - fs.props.temperature_ref) + fs.cstr.control_volume.heat[0.0] - fs.rxn.dh_rxn[R1]*fs.cstr.control_volume.rate_reaction_extent[t,R1] for t in fs._time (W)
controlsfs.cstr.control_volume.heat (piecewise_constant, free, W), fs.cstr._flow_vol_inlet_ref (piecewise_constant, free, m**3/s)
tracking stage costcost[t] == ((fs.cstr.control_volume.energy_holdup[t,'Liq'] - eng_ss[Liq])/scale_E)**2 + ((fs.cstr.control_volume.heat[t] - duty_ss)/scale_Q)**2 + ((_flow_vol_inlet_ref[t] - flow_ss)/scale_F)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','NaOH'] - ss_naoh)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','EthylAcetate'] - ss_ea)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','SodiumAcetate'] - ss_sa)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[t,'Liq','Ethanol'] - ss_etoh)/scale_M)**2 for t in sorted(fs._time)[:-1]
terminal costterm == ((fs.cstr.control_volume.material_holdup[10.0,Liq,NaOH] - ss_naoh)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,EthylAcetate] - ss_ea)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,SodiumAcetate] - ss_sa)/scale_M)**2 + ((fs.cstr.control_volume.material_holdup[10.0,Liq,Ethanol] - ss_etoh)/scale_M)**2 + ((fs.cstr.control_volume.energy_holdup[10.0,Liq] - eng_ss[Liq])/scale_E)**2
initial conditionsmat0[j] == fs.cstr.control_volume.material_holdup[0.0,'Liq',j] for j in OrderedScalarSet (mol)
initial conditionseng0[p] == fs.cstr.control_volume.energy_holdup[0.0,p] for p in fs.props.phase_list (J)
steady-state targetsss_naoh (of fs.cstr.control_volume.material_holdup_Liq_NaOH, mol), ss_ea (of fs.cstr.control_volume.material_holdup_Liq_EthylAcetate, mol), ss_sa (of fs.cstr.control_volume.material_holdup_Liq_SodiumAcetate, mol), ss_etoh (of fs.cstr.control_volume.material_holdup_Liq_Ethanol, mol), eng_ss (of fs.cstr.control_volume.energy_holdup, J)
steady-state control targetsduty_ss (of fs.cstr.control_volume.heat, W), flow_ss (of fs.cstr._flow_vol_inlet_ref, m**3/s)
transformations
  1. drto.infinite_horizon: segment=1 elements x 5 Legendre points, beta=1.2, gamma=0.04694445, profile=collocation, horizon=kept, infinite tail appended, algebraic=3 components replicated, blocks=3 time-indexed Block(s): 9 components, 3 equation families replicated, partial=algebraic entries of 2 indexed Var(s) copied per member, terminal_cost=terminal deactivated (the tail owns it), terminal=soft pin z(tau=1)=z_s on 5 states, mu=1000.0
  2. drto.parameterize: profiles=fs.cstr.control_volume.heat (piecewise_constant), fs.cstr._flow_vol_inlet_ref (piecewise_constant)
  3. drto.build_objective: objective=sum of 35 weighted cost terms
  4. drto.dynamic_optimization: horizon=kept, tracking_weight=(one stage cost declared), sensitivity=5 initial-condition Params declared
# drto-aware plotting: filled points on the finite horizon, open points on
# the terminal segment mapped back to real time, dotted setpoint lines from
# the declared pairings. With V = 1 m3 the material holdup reads as
# concentration, and the energy holdup is the temperature story in J.
plot_states(m);
plt.show()
../_images/c09aece50d72314b58c950e751a5fac996fa07d58b027e005c9f47ad1678798e.png
plot_controls(m);
plt.show()
../_images/0afa712ef16dee82d96bc03aed12966bd10191a15d35d3333eca7cc6f563f982.png
plot_stage_cost(m);
plt.show()
../_images/66fae274617bb27afd30cafd2c39e4b8dba3e5c36c0b0bc4df663e53b6f1464c.png