def generate_discretization_components_along_set(m, set_, active=True): # What is a "discretization equation with respect to 's'"? # Is it the component itself, or reference-to-slices of the # component along set s? # # If we consider a "variable" to be something that maps time # to some data, then a "differential variable" or "discretization # equation" should also map time to some data... for var in m.component_objects(pyo.Var, active=active): if (isinstance(var, dae.DerivativeVar) and set_ in ComponentSet(var.get_continuousset_list())): block = var.parent_block() # NOTE: Making some assumptions about the name and location # of discretization equations. con = block.find_component(var.local_name + DAE_DISC_SUFFIX) var_map = dict( (idx, pyo.Reference(slice_)) for idx, slice_ in slice_component_along_sets(var, (set_, ))) con_map = dict( (idx, pyo.Reference(slice_)) for idx, slice_ in slice_component_along_sets(con, (set_, ))) if not active or con.active: # We do not check the individual condata objects for activity... for idx, var in var_map.items(): yield idx, con_map[idx], var
def make_model(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=1.0) m.y = pyo.Var(initialize=1.0) m.u = pyo.Var(initialize=1.0) m.v = pyo.Var(initialize=1.0) m.con_1 = pyo.Constraint(expr=m.x * m.y == m.u) m.con_2 = pyo.Constraint(expr=m.x**2 * m.y**3 == m.v) m.con_3 = pyo.Constraint( expr=self.con_3_body(m.x, m.y, m.u, m.v) == self.con_3_rhs()) m.con_4 = pyo.Constraint( expr=self.con_4_body(m.x, m.y, m.u, m.v) == self.con_4_rhs()) epm_model = pyo.ConcreteModel() epm_model.x = pyo.Reference(m.x) epm_model.y = pyo.Reference(m.y) epm_model.u = pyo.Reference(m.u) epm_model.v = pyo.Reference(m.v) epm_model.epm = ExternalPyomoModel( [m.u, m.v], [m.x, m.y], [m.con_3, m.con_4], [m.con_1, m.con_2], ) epm_model.obj = pyo.Objective(expr=m.x**2 + m.y**2 + m.u**2 + m.v**2) epm_model.egb = ExternalGreyBoxBlock() epm_model.egb.set_external_model(epm_model.epm, inputs=[m.u, m.v]) return epm_model
def test_extra_category(self): model = make_model(horizon=1, nfe=2) time = model.time t0 = time.first() inputs = [model.flow_in] measurements = [ pyo.Reference(model.conc[:, 'A']), pyo.Reference(model.conc[:, 'B']), ] disturbances = [ pyo.Reference(model.conc_in[:, 'A']), pyo.Reference(model.conc_in[:, 'B']), ] category_dict = { VC.INPUT: inputs, VC.MEASUREMENT: measurements, VC.DISTURBANCE: disturbances, } db = DynamicBlock( model=model, time=time, category_dict={None: category_dict}, ) db.construct() db.set_sample_time(0.5) db.vectors.input[:, t0].set_value(1.1) db.vectors.measurement[:, t0].set_value(2.2) db.vectors.disturbance[:, t0].set_value(3.3) assert model.flow_in[t0].value == 1.1 assert model.conc[t0, 'A'].value == 2.2 assert model.conc[t0, 'B'].value == 2.2 assert model.conc_in[t0, 'A'].value == 3.3 assert model.conc_in[t0, 'B'].value == 3.3
def test_empty_category(self): model = make_model(horizon=1, nfe=2) time = model.time t0 = time.first() inputs = [model.flow_in] measurements = [ pyo.Reference(model.conc[:, 'A']), pyo.Reference(model.conc[:, 'B']), ] category_dict = { VC.INPUT: inputs, VC.MEASUREMENT: measurements, VC.ALGEBRAIC: [], } db = DynamicBlock( model=model, time=time, category_dict={None: category_dict}, ) db.construct() db.set_sample_time(0.5) # Categories with no variables are removed. # If they were retained, trying to iterate # over, e.g., vectors.algebraic[:, :] would # fail due to inconsistent dimension. assert VC.ALGEBRAIC not in db.category_dict
def test_tag_ref(model): m = model m.rw = pyo.Reference(m.w[:, "a"]) m.ry = pyo.Reference(m.y) rw = ModelTag(expr=m.rw, format_string="{:.3f}", doc="Tag for rw") ry = ModelTag(expr=m.ry, format_string="{:.2f}", doc="Tag for ry") m.w[1, "a"] = 3 assert str(rw[1]) == "3.000 kg" assert rw[1].is_var ry[:].set(2) assert str(ry[None]) == "2.00 s"
def test_reference(self): m = pyo.ConcreteModel() m.v1 = pyo.Var() m.ref = pyo.Reference(m.v1) m.c1 = pyo.Constraint(expr=m.v1 == 1.0) igraph = IncidenceGraphInterface(m) self.assertEqual(igraph.incidence_matrix.shape, (1, 1))
def pressure_flow_default_callback(valve): """ Add the default pressure flow relation constraint. This will be used in the valve model, a custom callback is provided. """ umeta = valve.config.property_package.get_metadata().get_derived_units valve.Cv = pyo.Var(initialize=0.1, doc="Valve flow coefficent", units=umeta("amount") / umeta("time") / umeta("pressure")**0.5) valve.Cv.fix() valve.flow_var = pyo.Reference( valve.control_volume.properties_in[:].flow_mol) valve.pressure_flow_equation_scale = lambda x: x**2 @valve.Constraint(valve.flowsheet().config.time) def pressure_flow_equation(b, t): Po = b.control_volume.properties_out[t].pressure Pi = b.control_volume.properties_in[t].pressure F = b.control_volume.properties_in[t].flow_mol Cv = b.Cv fun = b.valve_function[t] return F**2 == Cv**2 * (Pi - Po) * fun**2
def __init__(self, expr, format_string="{}", doc="", display_units=None): """initialize a model tag instance. Args: expr: A Pyomo Var, Expression, Param, Reference, or unnamed expression to tag. This can be a scalar or indexed. format_string: A formating string used to print an elememnt of the tagged expression (e.g. '{:.3f}'). doc: A description of the tagged quantity. display_units: Pyomo units to display the quantity in. If a string is provided it will be used to display as the unit, but will not be used to convert units. If None, use native units of the quantity. """ super().__init__() self._format = format_string # format string for printing expression if isinstance(expr, IndexedComponent_slice): self._expression = pyo.Reference( expr) # tag expression (can be unnamed) else: self._expression = expr self._doc = doc # documentation for a tag self._display_units = display_units # unit to display value in self._cache_validation_value = {} # value when converted value stored self._cache_display_value = { } # value to display after unit conversions self._name = None # tag name (just used to claify error messages) self._root = None # use this to cache scalar tags in indexed parent self._index = None # index to get cached converted value from parent self._group = None # Group object if this is a member of a group self._str_units = True # include units to stringify the tag # if _set_in_display_units is True and no units are provided for for # set, fix, setub, and setlb, the units will be assumed to be the # display units. If it is false and no units are proided, the units are # assumed to be the native units of the quantity self._set_in_display_units = False
def plot_outlet_states_over_time( m, show=True, prefix=None, extra_states=None, ): if prefix is None: prefix = "" if extra_states is None: extra_states = [] x0 = m.fs.MB.gas_phase.length_domain.first() x1 = m.fs.MB.solid_phase.length_domain.last() state_slices = [ m.fs.MB.gas_phase.properties[:, x1].temperature, m.fs.MB.gas_phase.properties[:, x1].pressure, m.fs.MB.gas_phase.properties[:, x1].flow_mol, m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["CH4"], m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["H2O"], m.fs.MB.gas_phase.properties[:, x1].mole_frac_comp["CO2"], m.fs.MB.solid_phase.properties[:, x0].temperature, m.fs.MB.solid_phase.properties[:, x0].flow_mass, m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Fe2O3"], m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Fe3O4"], m.fs.MB.solid_phase.properties[:, x0].mass_frac_comp["Al2O3"], ] state_refs = [pyo.Reference(slice_) for slice_ in state_slices] state_refs.extend(extra_states) for i, ref in enumerate(state_refs): fig, ax = plot_time_indexed_variable(ref) fig.savefig(prefix + "state%s.png" % i, transparent=True) if show: plt.show()
def test_tag_group(model): m = model g = ModelTagGroup() m.rw = pyo.Reference(m.w[:, "a"]) g.add("w", expr=m.rw, format_string="{:.3f}", doc="make sure this works", display_units=pyo.units.g) g.add("x", expr=m.x, format_string="{:.3f}") g.add("y", expr=m.y, format_string="{:.3f}") g.add("z", expr=m.z, format_string="{:.3f}") g.add("e", expr=m.e, format_string="{:.3f}") g.add("f", ModelTag(expr=m.f, format_string="{:.3f}")) g.add("g", expr=m.g, format_string="{:.1f}", display_units="%") assert g["w"].doc == "make sure this works" g.str_include_units = True assert g.str_include_units assert g["w"].str_include_units assert not g.set_in_display_units assert not g["w"].set_in_display_units g["w"].fix(2) g["x"].fix(1) g["y"].fix(3) assert str(g["w"][1]) == "2000.000 g" assert str(g["x"][1]) == "1.000 kg" assert str(g["y"]) == "3.000 s" g.str_include_units = False assert str(g["w"][1]) == "2000.000" assert str(g["x"][1]) == "1.000" assert str(g["y"]) == "3.000" g.set_in_display_units = True g["w"][:].set(1000) assert str(g["w"][1]) == "1000.000" assert str(g["w"][2]) == "1000.000" assert str(g["w"][2]) == "1000.000" g.set_in_display_units = True g["w"][:].set(2 * pyo.units.kg) assert str(g["w"][1]) == "2000.000" assert str(g["w"][2]) == "2000.000" assert str(g["w"][2]) == "2000.000" g["w"][:].fix(3 * pyo.units.kg) assert str(g["w"][1]) == "3000.000" assert str(g["w"][2]) == "3000.000" assert str(g["w"][2]) == "3000.000" g["w"][:].fix(4000) assert str(g["w"][1]) == "4000.000" assert str(g["w"][2]) == "4000.000" assert str(g["w"][2]) == "4000.000"
def generate_diff_deriv_disc_components_along_set(m, set_, active=True): for var in m.component_objects(pyo.Var, active=active): if (isinstance(var, dae.DerivativeVar) and set_ in ComponentSet(var.get_continuousset_list())): block = var.parent_block() con = block.find_component(var.local_name + DAE_DISC_SUFFIX) state = var.get_state_var() deriv_map = dict( (idx, pyo.Reference(slice_)) for idx, slice_ in slice_component_along_sets(var, (set_, ))) disc_map = dict( (idx, pyo.Reference(slice_)) for idx, slice_ in slice_component_along_sets(con, (set_, ))) state_map = dict( (idx, pyo.Reference(slice_)) for idx, slice_ in slice_component_along_sets(state, (set_, ))) if not active or con.active: for idx, deriv in deriv_map.items(): yield state_map[idx], deriv, disc_map[idx]
def make_model(): m = pyo.ConcreteModel() m.time = pyo.Set(initialize=range(11)) m.space = pyo.Set(initialize=range(3)) m.comp = pyo.Set(initialize=['a', 'b']) m.v1 = pyo.Var(m.time, m.space) m.v2 = pyo.Var(m.time, m.space, m.comp) @m.Block(m.time, m.space) def b(b, t, x): b.v3 = pyo.Var() b.v4 = pyo.Var(m.comp) m.v1_refs = [pyo.Reference(m.v1[:, x]) for x in m.space] m.v2a_refs = [pyo.Reference(m.v2[:, x, 'a']) for x in m.space] m.v2b_refs = [pyo.Reference(m.v2[:, x, 'b']) for x in m.space] m.v3_refs = [pyo.Reference(m.b[:, x].v3) for x in m.space] m.v4a_refs = [pyo.Reference(m.b[:, x].v4['a']) for x in m.space] m.v4b_refs = [pyo.Reference(m.b[:, x].v4['a']) for x in m.space] for t in m.time: for ref in m.v1_refs: ref[t] = 1.0 * t for ref in m.v2a_refs + m.v2b_refs: ref[t] = 2.0 * t for ref in m.v3_refs: ref[t] = 3.0 * t for ref in m.v4a_refs + m.v4b_refs: ref[t] = 4.0 * t return m
def test_initialize_only_measurement_input(self): model = make_model(horizon=1, nfe=2) time = model.time t0 = time.first() inputs = [model.flow_in] measurements = [ pyo.Reference(model.conc[:, 'A']), pyo.Reference(model.conc[:, 'B']), ] category_dict = { VC.INPUT: inputs, VC.MEASUREMENT: measurements, } db = DynamicBlock( model=model, time=time, category_dict={None: category_dict}, ) db.construct() db.set_sample_time(0.5) db.mod.flow_in[:].set_value(3.0) initialize_t0(db.mod) copy_values_forward(db.mod) db.mod.flow_in[:].set_value(2.0) # Don't need to know any of the special categories to initialize # by element. This is only because we have an implicit discretization. db.initialize_by_solving_elements(solver) t0 = time.first() tl = time.last() vectors = db.vectors assert vectors.input[0, tl].value == 2.0 assert vectors.measurement[0, tl].value == pytest.approx( 3.185595567867036) assert vectors.measurement[1, tl].value == pytest.approx( 1.1532474073395755) assert model.dcdt[tl, 'A'].value == pytest.approx(0.44321329639889284) assert model.dcdt[tl, 'B'].value == pytest.approx(0.8791007531878847)
def test_NmpcVector(): m = pyo.ConcreteModel() m.coords = pyo.Set(initialize=[0, 1, 2, 3]) m.time = pyo.Set(initialize=[0.0, 0.5, 1.0, 1.5, 2.0]) @m.Block(m.coords) def b(b, i): b.var = NmpcVar(m.time) m.vector = pyo.Reference(m.b[:].var[:], ctype=_NmpcVector) assert type(m.vector) is _NmpcVector # Test that `vector` is a proper reference for i, t in m.coords * m.time: assert m.vector[i, t] is m.b[i].var[t] # Test that we can generate the underlying NmpcVars for v, i in zip(m.vector._generate_referenced_vars(), m.coords): assert v is m.b[i].var # `set_setpoint` m.vector.set_setpoint(3.14) for i in m.coords: assert m.b[i].var.setpoint == 3.14 setpoint = tuple(i / 10. for i in m.coords) m.vector.set_setpoint(setpoint) for i, sp in zip(m.coords, setpoint): assert m.b[i].var.setpoint == sp # `get_setpoint` sp_get = tuple(m.vector.get_setpoint()) assert setpoint == sp_get # `set_values` val = -1. m.vector.values = -1. for i, t in m.coords * m.time: assert m.b[i].var[t].value == val newvals = tuple(i * 1.1 for i in m.coords) m.vector.values = newvals for i, val in zip(m.coords, newvals): for t in m.time: assert m.b[i].var[t].value == val # `get_values` val_lil = m.vector.values for var_values, target_val in zip(val_lil, newvals): for var_val in var_values: assert var_val == target_val
def reshape(m): time = m.time # Create block to hold time-decomposition m.time_block = pyo.Block(time) # Identify time-indexed components scalar_vars, dae_vars = flatten_dae_components(m, time, pyo.Var) scalar_cons, dae_cons = flatten_dae_components(m, time, pyo.Constraint) for t in time: b = m.time_block[t] var_list = [] b.vars = pyo.Reference([var[t] for var in dae_vars]) con_list = [] for con in dae_cons: try: condata = con[t] con_list.append(condata) except KeyError: # For discretization equations, which are skipped at t0 pass b.cons = pyo.Reference(con_list)
def TagReference(s, description=""): """ Create a Pyomo reference with an added description string attribute to describe the reference. The intended use for these references is to create a time-indexed reference to variables in a model corresponding to plant measurment tags. Args: s: Pyomo time slice of a variable or expression description (str): A description the measurment Returns: A Pyomo Reference object with an added doc attribute """ r = pyo.Reference(s) r.description = description return r
def build(self): """ Add model equations to the unit model. This is called by a default block construnction rule when the unit model is created. """ super().build() # Basic unit model build/read config config = self.config # shorter config pointer # The thermodynamic expression writer object, te, writes expressions # including external function calls to calculate thermodynamic quantities # from a set of state variables. _assert_properties(config.property_package) te = ThermoExpr(blk=self, parameters=config.property_package) eff = self.efficiency_pump = pyo.Var(self.flowsheet().config.time, initialize=0.9, doc="Pump efficiency") self.efficiency_isentropic = pyo.Reference(self.efficiency_pump[:]) pratio = self.ratioP = pyo.Var(self.flowsheet().config.time, initialize=0.7, doc="Ratio of outlet to inlet pressure") # Some shorter refernces to property blocks properties_in = self.control_volume.properties_in properties_out = self.control_volume.properties_out @self.Expression(self.flowsheet().config.time, doc="Thermodynamic work") def work_fluid(b, t): return properties_out[t].flow_vol * (self.deltaP[t]) @self.Expression(self.flowsheet().config.time, doc="Work required to drive the pump.") def shaft_work(b, t): # Early access to the outlet enthalpy and work return self.work_fluid[t] / eff[t] @self.Constraint(self.flowsheet().config.time) def eq_work(b, t): # outlet enthalpy coens from energy balance return self.control_volume.work[t] == self.shaft_work[t] @self.Constraint(self.flowsheet().config.time) def eq_pressure_ratio(b, t): return (pratio[t] * properties_in[t].pressure == properties_out[t].pressure)
def test_9(): # test with references the way we handle time indexing a lot in IDAES rp = pyo.TransformationFactory("replace_variables") block_set = {1,2,3} m = pyo.ConcreteModel() m.b1 = pyo.Block(block_set) for i in block_set: m.b1[i].x = pyo.Var(initialize=2) m.y = pyo.Var([1,2,3], initialize=3) m.xx = pyo.Reference(m.b1[:].x) m.display() m.e1 = pyo.Expression(expr=sum(m.xx[i] for i in m.xx)) m.e2 = pyo.Expression(expr=sum(m.b1[i].x for i in m.b1)) assert(pyo.value(m.e1) == 6) assert(pyo.value(m.e2) == 6) rp.apply_to(m, substitute=[(m.xx, m.y)]) assert(pyo.value(m.e1) == 9) assert(pyo.value(m.e2) == 9)
def m(self): m = pyo.ConcreteModel() m.a = pyo.Var(initialize=0.25, bounds=(-0.5, 0.5)) m.b = b = pyo.Block() b.a = pyo.Var([1, 2], bounds=(-10, 10)) m.c = pyo.Constraint(expr=(0, 1. / (m.a**2), 100)) b.c = pyo.Constraint([1, 2], rule=lambda b, i: (i - 1, b.a[i], i + 2)) b.d = pyo.Constraint(expr=b.a[1]**4 + b.a[2]**4 <= 4) set_scaling_factor(b.d, 1e6) b.o = pyo.Expression(expr=sum(b.a)**2) m.o = pyo.Objective(expr=m.a + b.o) # references are tricky, could cause a variable # to be iterated over several times in # component_data_objects m.b_a = pyo.Reference(b.a) return m
def _valve_pressure_flow_cb(b): """ For vapor F = Cv*sqrt(Pi**2 - Po**2)*f(x) """ umeta = b.config.property_package.get_metadata().get_derived_units b.Cv = pyo.Var(initialize=0.1, doc="Valve flow coefficent", units=umeta("amount") / umeta("time") / umeta("pressure")) b.Cv.fix() b.flow_var = pyo.Reference(b.control_volume.properties_in[:].flow_mol) b.pressure_flow_equation_scale = lambda x: x**2 @b.Constraint(b.flowsheet().time) def pressure_flow_equation(b2, t): Po = b2.control_volume.properties_out[t].pressure Pi = b2.control_volume.properties_in[t].pressure F = b2.control_volume.properties_in[t].flow_mol Cv = b2.Cv fun = b2.valve_function[t] return F**2 == Cv**2 * (Pi**2 - Po**2) * fun**2
def update_metadata_model_references(model, metadata): """ Create model references from refernce strings in the metadata. This updates the 'reference' field in the metadata. Args: model (pyomo.environ.Block): Pyomo model metadata (dict): Tag metadata dictionary Returns: None """ for tag, md in metadata.items(): if md["reference_string"]: try: md["reference"] = pyo.Reference( eval(md["reference_string"], {"m": model})) except (KeyError, AttributeError, NameError): warnings.warn( "Tag reference {} not found".format( md["reference_string"]), UserWarning, )
def main(plot_switch=False): # This tests the same model constructed in the test_nmpc_constructor_1 file m_controller = make_model(horizon=3, ntfe=30, ntcp=2, bounds=True) sample_time = 0.5 m_plant = make_model(horizon=sample_time, ntfe=5, ntcp=2) time_plant = m_plant.fs.time solve_consistent_initial_conditions(m_plant, time_plant, solver) ##### # Flatten and categorize controller model ##### model = m_controller time = model.fs.time t0 = time.first() t1 = time[2] scalar_vars, dae_vars = flatten_dae_components( model, time, pyo.Var, ) scalar_cons, dae_cons = flatten_dae_components( model, time, pyo.Constraint, ) inputs = [ model.fs.mixer.S_inlet.flow_vol, model.fs.mixer.E_inlet.flow_vol, ] measurements = [ pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']), model.fs.cstr.outlet.temperature, ] model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix() model.fs.cstr.total_flow_balance.deactivate() var_partition, con_partition = categorize_dae_variables_and_constraints( model, dae_vars, dae_cons, time, input_vars=inputs, ) controller = ControllerBlock( model=model, time=time, measurements=measurements, category_dict={None: var_partition}, ) controller.construct() solve_consistent_initial_conditions(m_controller, time, solver) controller.initialize_to_initial_conditions() m_controller._dummy_obj = pyo.Objective(expr=0) nlp = PyomoNLP(m_controller) igraph = IncidenceGraphInterface(nlp) m_controller.del_component(m_controller._dummy_obj) diff_vars = [var[t1] for var in var_partition[VC.DIFFERENTIAL]] alg_vars = [var[t1] for var in var_partition[VC.ALGEBRAIC]] deriv_vars = [var[t1] for var in var_partition[VC.DERIVATIVE]] diff_eqns = [con[t1] for con in con_partition[CC.DIFFERENTIAL]] alg_eqns = [con[t1] for con in con_partition[CC.ALGEBRAIC]] # Assemble and factorize "derivative Jacobian" dfdz = nlp.extract_submatrix_jacobian(diff_vars, diff_eqns) dfdy = nlp.extract_submatrix_jacobian(alg_vars, diff_eqns) dgdz = nlp.extract_submatrix_jacobian(diff_vars, alg_eqns) dgdy = nlp.extract_submatrix_jacobian(alg_vars, alg_eqns) dfdzdot = nlp.extract_submatrix_jacobian(deriv_vars, diff_eqns) fact = sps.linalg.splu(dgdy.tocsc()) dydz = fact.solve(dgdz.toarray()) deriv_jac = dfdz - dfdy.dot(dydz) fact = sps.linalg.splu(dfdzdot.tocsc()) dzdotdz = -fact.solve(deriv_jac) # Use some heuristic on the eigenvalues of the derivative Jacobian # to identify fast states. w, V = np.linalg.eig(dzdotdz) w_max = np.max(np.abs(w)) fast_modes, = np.where(np.abs(w) > w_max / 2) fast_states = [] for idx in fast_modes: evec = V[:, idx] _fast_states, _ = np.where(np.abs(evec) > 0.5) fast_states.extend(_fast_states) fast_states = set(fast_states) # Store components necessary for model reduction in a model- # independent form. fast_state_derivs = [ pyo.ComponentUID(var_partition[VC.DERIVATIVE][idx].referent, context=model) for idx in fast_states ] fast_state_diffs = [ pyo.ComponentUID(var_partition[VC.DIFFERENTIAL][idx].referent, context=model) for idx in fast_states ] fast_state_discs = [ pyo.ComponentUID(con_partition[CC.DISCRETIZATION][idx].referent, context=model) for idx in fast_states ] # Perform pseudo-steady state model reduction on the fast states # and re-categorize for cuid in fast_state_derivs: var = cuid.find_component_on(m_controller) var.fix(0.0) for cuid in fast_state_diffs: var = cuid.find_component_on(m_controller) var[t0].unfix() for cuid in fast_state_discs: con = cuid.find_component_on(m_controller) con.deactivate() var_partition, con_partition = categorize_dae_variables_and_constraints( model, dae_vars, dae_cons, time, input_vars=inputs, ) controller.del_component(model) # Re-construct controller block with new categorization measurements = [ pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']), ] controller = ControllerBlock( model=model, time=time, measurements=measurements, category_dict={None: var_partition}, ) controller.construct() ##### # Construct dynamic block for plant ##### model = m_plant time = model.fs.time t0 = time.first() t1 = time[2] scalar_vars, dae_vars = flatten_dae_components( model, time, pyo.Var, ) scalar_cons, dae_cons = flatten_dae_components( model, time, pyo.Constraint, ) inputs = [ model.fs.mixer.S_inlet.flow_vol, model.fs.mixer.E_inlet.flow_vol, ] measurements = [ pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'C']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'E']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'S']), pyo.Reference(model.fs.cstr.outlet.conc_mol[:, 'P']), ] model.fs.cstr.control_volume.material_holdup[:, 'aq', 'Solvent'].fix() model.fs.cstr.total_flow_balance.deactivate() var_partition, con_partition = categorize_dae_variables_and_constraints( model, dae_vars, dae_cons, time, input_vars=inputs, ) plant = DynamicBlock( model=model, time=time, measurements=measurements, category_dict={None: var_partition}, ) plant.construct() p_t0 = plant.time.first() c_t0 = controller.time.first() p_ts = plant.sample_points[1] c_ts = controller.sample_points[1] controller.set_sample_time(sample_time) plant.set_sample_time(sample_time) # We now perform the "RTO" calculation: Find the optimal steady state # to achieve the following setpoint setpoint = [ (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 0.4), #(controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.01), (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 0.1), (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 300), (controller.mod.fs.mixer.E_inlet.flow_vol[0], 0.1), (controller.mod.fs.mixer.S_inlet.flow_vol[0], 2.0), (controller.mod.fs.cstr.volume[0], 1.0), ] setpoint_weights = [ (controller.mod.fs.cstr.outlet.conc_mol[0, 'P'], 1.), (controller.mod.fs.cstr.outlet.conc_mol[0, 'S'], 1.), (controller.mod.fs.cstr.control_volume.energy_holdup[0, 'aq'], 1.), (controller.mod.fs.mixer.E_inlet.flow_vol[0], 1.), (controller.mod.fs.mixer.S_inlet.flow_vol[0], 1.), (controller.mod.fs.cstr.volume[0], 1.), ] # Some of the "differential variables" that have been fixed in the # model file are different from the measurements listed above. We # unfix them here so the RTO solve is not overconstrained. # (The RTO solve will only automatically unfix inputs and measurements.) controller.mod.fs.cstr.control_volume.material_holdup[0, ...].unfix() controller.mod.fs.cstr.control_volume.energy_holdup[0, ...].unfix() #controller.mod.fs.cstr.volume[0].unfix() controller.mod.fs.cstr.control_volume.material_holdup[0, 'aq', 'Solvent'].fix() controller.add_setpoint_objective(setpoint, setpoint_weights) controller.solve_setpoint(solver) # Now we are ready to construct the tracking NMPC problem tracking_weights = [ *((v, 1.) for v in controller.vectors.differential[:, 0]), *((v, 1.) for v in controller.vectors.input[:, 0]), ] controller.add_tracking_objective(tracking_weights) controller.constrain_control_inputs_piecewise_constant() controller.initialize_to_initial_conditions() # Solve the first control problem controller.vectors.input[...].unfix() controller.vectors.input[:, 0].fix() solver.solve(controller, tee=True) # For a proper NMPC simulation, we must have noise. # We do this by treating inputs and measurements as Gaussian random # variables with the following variances (and bounds). cstr = controller.mod.fs.cstr variance = [ (cstr.outlet.conc_mol[0.0, 'S'], 0.01), (cstr.outlet.conc_mol[0.0, 'E'], 0.005), (cstr.outlet.conc_mol[0.0, 'C'], 0.01), (cstr.outlet.conc_mol[0.0, 'P'], 0.005), (cstr.outlet.temperature[0.0], 1.), (cstr.volume[0.0], 0.05), ] controller.set_variance(variance) measurement_variance = [ v.variance for v in controller.MEASUREMENT_BLOCK[:].var ] measurement_noise_bounds = [(0.0, var[c_t0].ub) for var in controller.MEASUREMENT_BLOCK[:].var] mx = plant.mod.fs.mixer variance = [ (mx.S_inlet_state[0.0].flow_vol, 0.02), (mx.E_inlet_state[0.0].flow_vol, 0.001), ] plant.set_variance(variance) input_variance = [v.variance for v in plant.INPUT_BLOCK[:].var] input_noise_bounds = [(0.0, var[p_t0].ub) for var in plant.INPUT_BLOCK[:].var] random.seed(100) # Extract inputs from controller and inject them into plant inputs = controller.generate_inputs_at_time(c_ts) plant.inject_inputs(inputs) # This "initialization" really simulates the plant with the new inputs. plant.vectors.input[:, :].fix() plant.initialize_by_solving_elements(solver) plant.vectors.input[:, :].fix() solver.solve(plant, tee=True) for i in range(1, 11): print('\nENTERING NMPC LOOP ITERATION %s\n' % i) measured = plant.generate_measurements_at_time(p_ts) plant.advance_one_sample() plant.initialize_to_initial_conditions() measured = apply_noise_with_bounds( measured, measurement_variance, random.gauss, measurement_noise_bounds, ) controller.advance_one_sample() controller.load_measurements(measured) solver.solve(controller, tee=True) inputs = controller.generate_inputs_at_time(c_ts) inputs = apply_noise_with_bounds( inputs, input_variance, random.gauss, input_noise_bounds, ) plant.inject_inputs(inputs) plant.initialize_by_solving_elements(solver) solver.solve(plant) import pdb pdb.set_trace()
def build(self): """ Building model Args: None Returns: None """ ######################################################################## # Call UnitModel.build to setup dynamics and configure # ######################################################################## super().build() self._process_config() config = self.config time = self.flowsheet().config.time ######################################################################## # Add control volumes # ######################################################################## hot_side = _make_heater_control_volume( self, config.hot_side_name, config.hot_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) cold_side = _make_heater_control_volume( self, config.cold_side_name, config.cold_side_config, dynamic=config.dynamic, has_holdup=config.has_holdup, ) # Add refernces to the hot side and cold side, so that we have solid # names to refere to internally. side_1 and side_2 also maintain # compatability with older models. Using add_object_reference keeps # these from showing up when you iterate through pyomo compoents in a # model, so only the user specified control volume names are "seen" if not hasattr(self, "side_1"): add_object_reference(self, "side_1", hot_side) if not hasattr(self, "side_2"): add_object_reference(self, "side_2", cold_side) if not hasattr(self, "hot_side"): add_object_reference(self, "hot_side", hot_side) if not hasattr(self, "cold_side"): add_object_reference(self, "cold_side", cold_side) ######################################################################## # Add variables # ######################################################################## # Use hot side units as basis s1_metadata = config.hot_side_config.property_package.get_metadata() f_units = s1_metadata.get_derived_units("flow_mole") cp_units = s1_metadata.get_derived_units("heat_capacity_mole") q_units = s1_metadata.get_derived_units("power") u_units = s1_metadata.get_derived_units("heat_transfer_coefficient") a_units = s1_metadata.get_derived_units("area") temp_units = s1_metadata.get_derived_units("temperature") self.overall_heat_transfer_coefficient = pyo.Var( time, domain=pyo.PositiveReals, initialize=100.0, doc="Overall heat transfer coefficient", units=u_units, ) self.area = pyo.Var( domain=pyo.PositiveReals, initialize=1000.0, doc="Heat exchange area", units=a_units, ) self.heat_duty = pyo.Reference(cold_side.heat) ######################################################################## # Add ports # ######################################################################## i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet", block=hot_side, doc="Hot side inlet") i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet", block=cold_side, doc="Cold side inlet") o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet", block=hot_side, doc="Hot side outlet") o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet", block=cold_side, doc="Cold side outlet") # Using Andrew's function for now. I want these port names for backward # compatablity, but I don't want them to appear if you iterate throught # components and add_object_reference hides them from Pyomo. if not hasattr(self, "inlet_1"): add_object_reference(self, "inlet_1", i1) if not hasattr(self, "inlet_2"): add_object_reference(self, "inlet_2", i2) if not hasattr(self, "outlet_1"): add_object_reference(self, "outlet_1", o1) if not hasattr(self, "outlet_2"): add_object_reference(self, "outlet_2", o2) if not hasattr(self, "hot_inlet"): add_object_reference(self, "hot_inlet", i1) if not hasattr(self, "cold_inlet"): add_object_reference(self, "cold_inlet", i2) if not hasattr(self, "hot_outlet"): add_object_reference(self, "hot_outlet", o1) if not hasattr(self, "cold_outlet"): add_object_reference(self, "cold_outlet", o2) ######################################################################## # Add a unit level energy balance # ######################################################################## @self.Constraint(time, doc="Heat balance equation") def unit_heat_balance(b, t): return 0 == (hot_side.heat[t] + pyunits.convert(cold_side.heat[t], to_units=q_units)) ######################################################################## # Add some useful expressions for condenser performance # ######################################################################## @self.Expression(time, doc="Inlet temperature difference") def delta_temperature_in(b, t): return (hot_side.properties_in[t].temperature - pyunits.convert( cold_side.properties_in[t].temperature, temp_units)) @self.Expression(time, doc="Outlet temperature difference") def delta_temperature_out(b, t): return (hot_side.properties_out[t].temperature - pyunits.convert( cold_side.properties_out[t].temperature, temp_units)) @self.Expression(time, doc="NTU Based temperature difference") def delta_temperature_ntu(b, t): return (hot_side.properties_in[t].temperature_sat - pyunits.convert(cold_side.properties_in[t].temperature, temp_units)) @self.Expression( time, doc="Minimum product of flow rate and heat " "capacity (always on tube side since shell side has phase change)") def mcp_min(b, t): return pyunits.convert( cold_side.properties_in[t].flow_mol * cold_side.properties_in[t].cp_mol_phase['Liq'], f_units * cp_units) @self.Expression(time, doc="Number of transfer units (NTU)") def ntu(b, t): return b.overall_heat_transfer_coefficient[t] * b.area / b.mcp_min[ t] @self.Expression(time, doc="Condenser effectiveness factor") def effectiveness(b, t): return 1 - pyo.exp(-self.ntu[t]) @self.Expression(time, doc="Heat treansfer") def heat_transfer(b, t): return b.effectiveness[t] * b.mcp_min[t] * b.delta_temperature_ntu[ t] ######################################################################## # Add Equations to calculate heat duty based on NTU method # ######################################################################## @self.Constraint(time, doc="Heat transfer rate equation based on NTU method") def heat_transfer_equation(b, t): return (pyunits.convert(cold_side.heat[t], q_units) == self.heat_transfer[t]) @self.Constraint( time, doc="Shell side outlet enthalpy is saturated water enthalpy") def saturation_eqn(b, t): return (hot_side.properties_out[t].enth_mol == hot_side.properties_in[t].enth_mol_sat_phase["Liq"])
def build(self): """ Build the PID block """ if isinstance(self.flowsheet().time, ContinuousSet): # time may not be a continuous set if you have a steady state model # in the steady state model case obviously the controller should # not be active, but you can still add it. if 'scheme' not in self.flowsheet().time.get_discretization_info(): # if you have a dynamic model, must do time discretization # before adding the PID model raise RunTimeError( "PIDBlock must be added after time discretization") super().build() # do the ProcessBlockData voodoo for config # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration requires 'pv'") if self.config.output is None: raise ConfigurationError( "Controller configuration requires 'output'") # Shorter pointers to time set information time_set = self.flowsheet().time t0 = time_set.first() # Variable for basic controller settings may change with time. self.setpoint = pyo.Var(time_set, doc="Setpoint") self.gain = pyo.Var(time_set, doc="Controller gain") self.time_i = pyo.Var(time_set, doc="Integral time") self.time_d = pyo.Var(time_set, doc="Derivative time") # Make the initial derivative term a variable so you can set it. This # should let you carry on from the end of another time period self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0) self.err_d0.fix() if not self.config.calculate_initial_integral: self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0) self.err_i0.fix() # Make references to the output and measured variables self.pv = pyo.Reference(self.config.pv) # No duplicate self.output = pyo.Reference(self.config.output) # No duplicate # Create an expression for error from setpoint @self.Expression(time_set, doc="Setpoint error") def err(b, t): return self.setpoint[t] - self.pv[t] # Use expressions to allow the some future configuration @self.Expression(time_set) def pterm(b, t): return -self.pv[t] @self.Expression(time_set) def dterm(b, t): return -self.pv[t] @self.Expression(time_set) def iterm(b, t): return self.err[t] # Output limits parameter self.limits = pyo.Param(["l", "h"], mutable=True, doc="controller output limits", initialize={ "l": self.config.lower, "h": self.config.upper }) # Smooth min and max are used to limit output, smoothing parameter here self.smooth_eps = pyo.Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") # This is ugly, but want integral and derivative error as expressions, # nice implementation with variables is harder to initialize and solve @self.Expression(time_set, doc="Derivative error.") def err_d(b, t): if t == t0: return self.err_d0 else: return (b.dterm[t] - b.dterm[time_set.prev(t)])\ /(t - time_set.prev(t)) if self.config.pid_form == PIDForm.standard: self._build_standard(time_set, t0) else: self._build_velocity(time_set, t0) # Add the controller output constraint and limit it with smooth min/max e = self.smooth_eps h = self.limits["h"] l = self.limits["l"] @self.Constraint(time_set, doc="Controller output constraint") def output_constraint(b, t): if t == t0: return pyo.Constraint.Skip else: return self.output[t] ==\ smooth_min( smooth_max(self.unconstrained_output[t], l, e), h, e)
def test_scaling_without_rename(self): m = pyo.ConcreteModel() m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) m.v1 = pyo.Var(initialize=10) m.v2 = pyo.Var(initialize=20) m.v3 = pyo.Var(initialize=30) def c1_rule(m): return m.v1 == 1e6 m.c1 = pyo.Constraint(rule=c1_rule) def c2_rule(m): return m.v2 == 1e-4 m.c2 = pyo.Constraint(rule=c2_rule) m.scaling_factor[m.v1] = 1.0 m.scaling_factor[m.v2] = 0.5 m.scaling_factor[m.v3] = 0.25 m.scaling_factor[m.c1] = 1e-5 m.scaling_factor[m.c2] = 1e5 values = {} values[id(m.v1)] = (m.v1.value, m.scaling_factor[m.v1]) values[id(m.v2)] = (m.v2.value, m.scaling_factor[m.v2]) values[id(m.v3)] = (m.v3.value, m.scaling_factor[m.v3]) values[id(m.c1)] = (pyo.value(m.c1.body), m.scaling_factor[m.c1]) values[id(m.c2)] = (pyo.value(m.c2.body), m.scaling_factor[m.c2]) m.c2_ref = pyo.Reference(m.c2) m.v3_ref = pyo.Reference(m.v3) scale = pyo.TransformationFactory('core.scale_model') scale.apply_to(m, rename=False) self.assertTrue(hasattr(m, 'v1')) self.assertTrue(hasattr(m, 'v2')) self.assertTrue(hasattr(m, 'c1')) self.assertTrue(hasattr(m, 'c2')) orig_val, factor = values[id(m.v1)] self.assertAlmostEqual( m.v1.value, orig_val*factor, ) orig_val, factor = values[id(m.v2)] self.assertAlmostEqual( m.v2.value, orig_val*factor, ) orig_val, factor = values[id(m.c1)] self.assertAlmostEqual( pyo.value(m.c1.body), orig_val*factor, ) orig_val, factor = values[id(m.c2)] self.assertAlmostEqual( pyo.value(m.c2.body), orig_val*factor, ) orig_val, factor = values[id(m.v3)] self.assertAlmostEqual( m.v3_ref[None].value, orig_val*factor, ) # Note that because the model was not renamed, # v3_ref is still intact. lhs = m.c2.body monom_factor = lhs.arg(0) scale_factor = (m.scaling_factor[m.c2]/ m.scaling_factor[m.v2]) self.assertAlmostEqual( monom_factor, scale_factor, )
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_geometry() self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type,) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) self.flash = HelmPhaseSeparator( default={ "dynamic": False, "property_package": self.config.property_package, } ) self.mixer = Mixer( default={ "dynamic": False, "property_package": self.config.property_package, "inlet_list": ["FeedWater", "SaturatedWater"], "mixed_state_block": self.control_volume.properties_in, } ) # instead of creating a new block use control volume to return solution # Inlet Ports # FeedWater to Drum (from Pipe or Economizer) self.feedwater_inlet = Port(extends=self.mixer.FeedWater) # Sat water from water wall self.water_steam_inlet = Port(extends=self.flash.inlet) # Exit Ports # Liquid to Downcomer # self.liquid_outlet = Port(extends=self.mixer.outlet) self.add_outlet_port('liquid_outlet', self.control_volume) # Steam to superheaters self.steam_outlet = Port(extends=self.flash.vap_outlet) # constraint to make pressures of two inlets of drum mixer the same @self.Constraint(self.flowsheet().config.time, doc="Mixter pressure identical") def mixer_pressure_eqn(b, t): return b.mixer.SaturatedWater.pressure[t]*1e-6 == \ b.mixer.FeedWater.pressure[t]*1e-6 self.stream_flash_out = Arc( source=self.flash.liq_outlet, destination=self.mixer.SaturatedWater ) # Pyomo arc connect flash liq_outlet with mixer SaturatedWater inlet pyo.TransformationFactory("network.expand_arcs").apply_to(self) # Add object references self.volume = pyo.Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = pyo.Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = pyo.Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance()
def build(self): """ Build the PID block """ super().build() # do the ProcessBlockData voodoo for config # Check for required config if self.config.pv is None: raise ConfigurationError("Controller configuration requires 'pv'") if self.config.output is None: raise ConfigurationError( "Controller configuration requires 'output'") # Shorter pointers to time set information time_set = self.flowsheet().time t0 = time_set.first() # Variable for basic controller settings may change with time. self.setpoint = pyo.Var(time_set, doc="Setpoint") self.gain = pyo.Var(time_set, doc="Controller gain") self.time_i = pyo.Var(time_set, doc="Integral time") self.time_d = pyo.Var(time_set, doc="Derivative time") # Make the initial derivative term a variable so you can set it. This # should let you carry on from the end of another time period self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0) self.err_d0.fix() if not self.config.calculate_initial_integral: self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0) self.err_i0.fix() # Make references to the output and measured variables self.pv = pyo.Reference(self.config.pv) # No duplicate self.output = pyo.Reference(self.config.output) # No duplicate # Create an expression for error from setpoint @self.Expression(time_set, doc="Setpoint error") def err(b, t): return self.setpoint[t] - self.pv[t] # Use expressions to allow the some future configuration @self.Expression(time_set) def pterm(b, t): return -self.pv[t] @self.Expression(time_set) def dterm(b, t): return -self.pv[t] @self.Expression(time_set) def iterm(b, t): return self.err[t] # Output limits parameter self.limits = pyo.Param(["l", "h"], mutable=True, doc="controller output limits", initialize={ "l": self.config.lower, "h": self.config.upper }) # Smooth min and max are used to limit output, smoothing parameter here self.smooth_eps = pyo.Param( mutable=True, initialize=1e-4, doc="Smoothing parameter for controller output limits") # This is ugly, but want integral and derivative error as expressions, # nice implementation with variables is harder to initialize and solve @self.Expression(time_set, doc="Derivative error.") def err_d(b, t): if t == t0: return self.err_d0 else: return (b.dterm[t] - b.dterm[time_set.prev(t)])\ /(t - time_set.prev(t)) # Want to fix the output varaible at the first time step to make # solving easier. This calculates the initial integral error to line up # with the initial output value, keeps the controller from initially # jumping. if self.config.calculate_initial_integral: @self.Expression(doc="Initial integral error") def err_i0(b): return b.time_i[t0]*(b.output[0] - b.gain[t0]*b.pterm[t0]\ - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0] # integral error @self.Expression(time_set, doc="Integral error") def err_i(b, t_end): return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) * (t - time_set.prev(t)) / 2.0 for t in time_set if t <= t_end and t > t0) # Calculate the unconstrainted controller output @self.Expression(time_set, doc="Unconstrained contorler output") def unconstrained_output(b, t): return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] + b.time_d[t] * b.err_d[t]) # Add the controller output constraint and limit it with smooth min/max e = self.smooth_eps h = self.limits["h"] l = self.limits["l"] @self.Constraint(time_set, doc="Controller output constraint") def output_constraint(b, t): if t == t0: return pyo.Constraint.Skip else: return self.output[t] ==\ smooth_min( smooth_max(self.unconstrained_output[t], l, e), h, e)
def create_model(): """Create the flowsheet and add unit models. Fixing model inputs is done in a separate function to try to keep this fairly clean and easy to follow. Args: None Returns: (ConcreteModel) Steam cycle model """ ############################################################################ # Flowsheet and Properties # ############################################################################ m = pyo.ConcreteModel(name="Steam Cycle Model") m.fs = FlowsheetBlock(default={"dynamic": False}) # Add steady state flowsheet # A physical property parameter block for IAPWS-95 with pressure and enthalpy # (PH) state variables. Usually pressure and enthalpy state variables are # more robust especially when the phases are unknown. m.fs.prop_water = iapws95.Iapws95ParameterBlock( default={"phase_presentation": iapws95.PhaseType.MIX}) # A physical property parameter block with temperature, pressure and vapor # fraction (TPx) state variables. There are a few instances where the vapor # fraction is known and the temperature and pressure state variables are # preferable. m.fs.prop_water_tpx = iapws95.Iapws95ParameterBlock( default={ "phase_presentation": iapws95.PhaseType.LG, "state_vars": iapws95.StateVars.TPX, }) ############################################################################ # Turbine with fill-in reheat constraints # ############################################################################ # The TurbineMultistage class allows creation of the full turbine model by # providing several configuration options, including: throttle valves; # high-, intermediate-, and low-pressure sections; steam extractions; and # pressure driven flow. See the IDAES documentation for details. m.fs.turb = HelmTurbineMultistage( default={ "property_package": m.fs.prop_water, "num_parallel_inlet_stages": 4, # number of admission arcs "num_hp": 7, # number of high-pressure stages "num_ip": 10, # number of intermediate-pressure stages "num_lp": 11, # number of low-pressure stages "hp_split_locations": [4, 7], # hp steam extraction locations "ip_split_locations": [5, 10], # ip steam extraction locations "lp_split_locations": [4, 8, 10, 11 ], # lp steam extraction locations "hp_disconnect": [7], # disconnect hp from ip to insert reheater "ip_split_num_outlets": { 10: 3 }, # number of split streams (default is 2) }) # This model is only the steam cycle, and the reheater is part of the boiler. # To fill in the reheater gap, a few constraints for the flow, pressure drop, # and outlet temperature are added. A detailed boiler model can be coupled later. # # hp_split[7] is the splitter directly after the last HP stage. The splitter # outlet "outlet_1" is always taken to be the main steam flow through the turbine. # When the turbine model was instantiated the stream from the HP section to the IP # section was omitted, so the reheater could be inserted. # The flow constraint sets flow from outlet_1 of the splitter equal to # flow into the IP turbine. @m.fs.turb.Constraint(m.fs.time) def constraint_reheat_flow(b, t): return b.ip_stages[1].inlet.flow_mol[t] == b.hp_split[ 7].outlet_1.flow_mol[t] # Create a variable for pressure change in the reheater (assuming # reheat_delta_p should be negative). m.fs.turb.reheat_delta_p = pyo.Var(m.fs.time, initialize=0, units=pyo.units.Pa) # Add a constraint to calculate the IP section inlet pressure based on the # pressure drop in the reheater and the outlet pressure of the HP section. @m.fs.turb.Constraint(m.fs.time) def constraint_reheat_press(b, t): return (b.ip_stages[1].inlet.pressure[t] == b.hp_split[7].outlet_1.pressure[t] + b.reheat_delta_p[t]) # Create a variable for reheat temperature and fix it to the desired reheater # outlet temperature m.fs.turb.reheat_out_T = pyo.Var(m.fs.time, initialize=866, units=pyo.units.K) # Create a constraint for the IP section inlet temperature. @m.fs.turb.Constraint(m.fs.time) def constraint_reheat_temp(b, t): return (b.ip_stages[1].control_volume.properties_in[t].temperature == b.reheat_out_T[t]) ############################################################################ # Add Condenser/hotwell/condensate pump # ############################################################################ # Add a mixer for all the streams coming into the condenser. In this case the # main steam, and the boiler feed pump turbine outlet go to the condenser m.fs.condenser_mix = HelmMixer( default={ "momentum_mixing_type": MomentumMixingType.none, "inlet_list": ["main", "bfpt"], "property_package": m.fs.prop_water, }) # The pressure in the mixer comes from the connection to the condenser. All # the streams coming in and going out of the mixer are equal, but we created # the mixer with no calculation for the unit pressure. Here a constraint that # specifies that the mixer pressure is equal to the main steam pressure is # added. There is also a constraint that specifies the that BFP turbine outlet # pressure is the same as the condenser pressure. Combined with the stream # connections between units, these constraints effectively specify that the # mixer inlet and outlet streams all have the same pressure. @m.fs.condenser_mix.Constraint(m.fs.time) def mixer_pressure_constraint(b, t): return b.main_state[t].pressure == b.mixed_state[t].pressure # The condenser model uses the physical property model with TPx state # variables, while the rest of the model uses PH state variables. To # translate between the two property calculations, an extra port is added to # the mixer which contains temperature, pressure, and vapor fraction # quantities. m.fs.condenser_mix.outlet_tpx = Port( initialize={ "flow_mol": pyo.Reference(m.fs.condenser_mix.mixed_state[:].flow_mol), "temperature": pyo.Reference(m.fs.condenser_mix.mixed_state[:].temperature), "pressure": pyo.Reference(m.fs.condenser_mix.mixed_state[:].pressure), "vapor_frac": pyo.Reference(m.fs.condenser_mix.mixed_state[:].vapor_frac), }) # Add the heat exchanger model for the condenser. m.fs.condenser = HeatExchanger( default={ "delta_temperature_callback": delta_temperature_underwood_callback, "shell": { "property_package": m.fs.prop_water_tpx }, "tube": { "property_package": m.fs.prop_water }, }) m.fs.condenser.delta_temperature_out.fix(5) # Everything condenses so the saturation pressure determines the condenser # pressure. Deactivate the constraint that is used in the TPx version vapor # fraction constraint and fix vapor fraction to 0. m.fs.condenser.shell.properties_out[:].eq_complementarity.deactivate() m.fs.condenser.shell.properties_out[:].vapor_frac.fix(0) # There is some subcooling in the condenser, so we assume the condenser # pressure is actually going to be slightly higher than the saturation # pressure. m.fs.condenser.pressure_over_sat = pyo.Var( m.fs.time, initialize=500, doc="Pressure added to Psat in the condeser. This is to account for" "some subcooling. (Pa)", units=pyo.units.Pa) # Add a constraint for condenser pressure @m.fs.condenser.Constraint(m.fs.time) def eq_pressure(b, t): return (b.shell.properties_out[t].pressure == b.shell.properties_out[t].pressure_sat + b.pressure_over_sat[t]) # Extra port on condenser to hook back up to pressure-enthalpy properties m.fs.condenser.outlet_1_ph = Port( initialize={ "flow_mol": pyo.Reference(m.fs.condenser.shell.properties_out[:].flow_mol), "pressure": pyo.Reference(m.fs.condenser.shell.properties_out[:].pressure), "enth_mol": pyo.Reference(m.fs.condenser.shell.properties_out[:].enth_mol), }) # Add the condenser hotwell. In steady state a mixer will work. This is # where makeup water is added if needed. m.fs.hotwell = HelmMixer( default={ "momentum_mixing_type": MomentumMixingType.none, "inlet_list": ["condensate", "makeup"], "property_package": m.fs.prop_water, }) # The hotwell is assumed to be at the same pressure as the condenser. @m.fs.hotwell.Constraint(m.fs.time) def mixer_pressure_constraint(b, t): return b.condensate_state[t].pressure == b.mixed_state[t].pressure # Condensate pump (Use compressor model, since it is more robust if vapor form) m.fs.cond_pump = HelmIsentropicCompressor( default={"property_package": m.fs.prop_water}) ############################################################################ # Add low pressure feedwater heaters # ############################################################################ # All the feedwater heater sections will be set to use the Underwood # approximation for LMTD, so create the fwh_config dict to make the config # slightly cleaner fwh_config = { "delta_temperature_callback": delta_temperature_underwood_callback } # The feedwater heater model allows feedwater heaters with a desuperheat, # condensing, and subcooling section to be added an a reasonably simple way. # See the IDAES documentation for more information of configuring feedwater # heaters m.fs.fwh1 = FWH0D( default={ "has_desuperheat": False, "has_drain_cooling": False, "has_drain_mixer": True, "property_package": m.fs.prop_water, "condense": fwh_config, }) # pump for fwh1 condensate, to pump it ahead and mix with feedwater m.fs.fwh1_pump = HelmIsentropicCompressor( default={"property_package": m.fs.prop_water}) # Mix the FWH1 drain back into the feedwater m.fs.fwh1_return = HelmMixer( default={ "momentum_mixing_type": MomentumMixingType.none, "inlet_list": ["feedwater", "fwh1_drain"], "property_package": m.fs.prop_water, }) # Set the mixer pressure to the feedwater pressure @m.fs.fwh1_return.Constraint(m.fs.time) def mixer_pressure_constraint(b, t): return b.feedwater_state[t].pressure == b.mixed_state[t].pressure # Add the rest of the low pressure feedwater heaters m.fs.fwh2 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": True, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) m.fs.fwh3 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": True, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) m.fs.fwh4 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": False, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) ############################################################################ # Add deaerator and boiler feed pump (BFP) # ############################################################################ # The deaerator is basically an open tank with multiple inlets. For steady- # state, a mixer model is sufficient. m.fs.fwh5_da = HelmMixer( default={ "momentum_mixing_type": MomentumMixingType.none, "inlet_list": ["steam", "drain", "feedwater"], "property_package": m.fs.prop_water, }) @m.fs.fwh5_da.Constraint(m.fs.time) def mixer_pressure_constraint(b, t): # Not sure about deaerator pressure, so assume same as feedwater inlet return b.feedwater_state[t].pressure == b.mixed_state[t].pressure # Add the boiler feed pump and boiler feed pump turbine m.fs.bfp = HelmIsentropicCompressor( default={"property_package": m.fs.prop_water}) m.fs.bfpt = HelmTurbineStage(default={"property_package": m.fs.prop_water}) # The boiler feed pump outlet pressure is the same as the condenser @m.fs.Constraint(m.fs.time) def constraint_out_pressure(b, t): return (b.bfpt.control_volume.properties_out[t].pressure == b.condenser.shell.properties_out[t].pressure) # Instead of specifying a fixed efficiency, specify that the steam is just # starting to condense at the outlet of the boiler feed pump turbine. This # ensures approximately the right behavior in the turbine. With a fixed # efficiency, depending on the conditions you can get odd things like steam # fully condensing in the turbine. @m.fs.Constraint(m.fs.time) def constraint_out_enthalpy(b, t): return ( b.bfpt.control_volume.properties_out[t].enth_mol == b.bfpt.control_volume.properties_out[t].enth_mol_sat_phase["Vap"] - 200 * pyo.units.J / pyo.units.mol) # The boiler feed pump power is the same as the power generated by the # boiler feed pump turbine. This constraint determines the steam flow to the # BFP turbine. The turbine work is negative for power out, while pump work # is positive for power in. @m.fs.Constraint(m.fs.time) def constraint_bfp_power(b, t): return 0 == b.bfp.control_volume.work[t] + b.bfpt.control_volume.work[t] ############################################################################ # Add high pressure feedwater heaters # ############################################################################ m.fs.fwh6 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": True, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) m.fs.fwh7 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": True, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) m.fs.fwh8 = FWH0D( default={ "has_desuperheat": True, "has_drain_cooling": True, "has_drain_mixer": False, "property_package": m.fs.prop_water, "desuperheat": fwh_config, "cooling": fwh_config, "condense": fwh_config, }) ############################################################################ # Additional Constraints/Expressions # ############################################################################ # Add a few constraints to allow a for complete plant results despite the # lack of a detailed boiler model. # Boiler pressure drop m.fs.boiler_pressure_drop_fraction = pyo.Var( m.fs.time, initialize=0.01, doc="Fraction of pressure lost from boiler feed pump and turbine inlet", ) @m.fs.Constraint(m.fs.time) def boiler_pressure_drop(b, t): return (m.fs.bfp.control_volume.properties_out[t].pressure * (1 - b.boiler_pressure_drop_fraction[t]) == m.fs.turb.inlet_split.mixed_state[t].pressure) # Again, since the boiler is missing, set the flow of steam into the turbine # equal to the flow of feedwater out of the last feedwater heater. @m.fs.Constraint(m.fs.time) def close_flow(b, t): return (m.fs.bfp.control_volume.properties_out[t].flow_mol == m.fs.turb.inlet_split.mixed_state[t].flow_mol) # Calculate the amount of heat that is added in the boiler, including the # reheater. @m.fs.Expression(m.fs.time) def boiler_heat(b, t): return (b.turb.inlet_split.mixed_state[t].enth_mol * b.turb.inlet_split.mixed_state[t].flow_mol - b.fwh8.desuperheat.tube.properties_out[t].enth_mol * b.fwh8.desuperheat.tube.properties_out[t].flow_mol + b.turb.ip_stages[1].control_volume.properties_in[t].enth_mol * b.turb.ip_stages[1].control_volume.properties_in[t].flow_mol - b.turb.hp_split[7].outlet_1.enth_mol[t] * b.turb.hp_split[7].outlet_1.flow_mol[t]) # Calculate the efficiency of the steam cycle. This doesn't account for # heat loss in the boiler, so actual plant efficiency would be lower. @m.fs.Expression(m.fs.time) def steam_cycle_eff(b, t): return -100 * b.turb.power[t] / b.boiler_heat[t] ############################################################################ ## Create the stream Arcs ## ############################################################################ ############################################################################ # Connect turbine and condenser units # ############################################################################ m.fs.EXHST_MAIN = Arc(source=m.fs.turb.outlet_stage.outlet, destination=m.fs.condenser_mix.main) m.fs.condenser_mix_to_condenser = Arc(source=m.fs.condenser_mix.outlet_tpx, destination=m.fs.condenser.inlet_1) m.fs.COND_01 = Arc(source=m.fs.condenser.outlet_1_ph, destination=m.fs.hotwell.condensate) m.fs.COND_02 = Arc(source=m.fs.hotwell.outlet, destination=m.fs.cond_pump.inlet) ############################################################################ # Low pressure FWHs # ############################################################################ m.fs.EXTR_LP11 = Arc(source=m.fs.turb.lp_split[11].outlet_2, destination=m.fs.fwh1.drain_mix.steam) m.fs.COND_03 = Arc(source=m.fs.cond_pump.outlet, destination=m.fs.fwh1.condense.inlet_2) m.fs.FWH1_DRN1 = Arc(source=m.fs.fwh1.condense.outlet_1, destination=m.fs.fwh1_pump.inlet) m.fs.FWH1_DRN2 = Arc(source=m.fs.fwh1_pump.outlet, destination=m.fs.fwh1_return.fwh1_drain) m.fs.FW01A = Arc(source=m.fs.fwh1.condense.outlet_2, destination=m.fs.fwh1_return.feedwater) # fwh2 m.fs.FW01B = Arc(source=m.fs.fwh1_return.outlet, destination=m.fs.fwh2.cooling.inlet_2) m.fs.FWH2_DRN = Arc(source=m.fs.fwh2.cooling.outlet_1, destination=m.fs.fwh1.drain_mix.drain) m.fs.EXTR_LP10 = Arc( source=m.fs.turb.lp_split[10].outlet_2, destination=m.fs.fwh2.desuperheat.inlet_1, ) # fwh3 m.fs.FW02 = Arc(source=m.fs.fwh2.desuperheat.outlet_2, destination=m.fs.fwh3.cooling.inlet_2) m.fs.FWH3_DRN = Arc(source=m.fs.fwh3.cooling.outlet_1, destination=m.fs.fwh2.drain_mix.drain) m.fs.EXTR_LP8 = Arc(source=m.fs.turb.lp_split[8].outlet_2, destination=m.fs.fwh3.desuperheat.inlet_1) # fwh4 m.fs.FW03 = Arc(source=m.fs.fwh3.desuperheat.outlet_2, destination=m.fs.fwh4.cooling.inlet_2) m.fs.FWH4_DRN = Arc(source=m.fs.fwh4.cooling.outlet_1, destination=m.fs.fwh3.drain_mix.drain) m.fs.EXTR_LP4 = Arc(source=m.fs.turb.lp_split[4].outlet_2, destination=m.fs.fwh4.desuperheat.inlet_1) ############################################################################ # FWH5 (Deaerator) and boiler feed pump (BFP) # ############################################################################ m.fs.FW04 = Arc(source=m.fs.fwh4.desuperheat.outlet_2, destination=m.fs.fwh5_da.feedwater) m.fs.EXTR_IP10 = Arc(source=m.fs.turb.ip_split[10].outlet_2, destination=m.fs.fwh5_da.steam) m.fs.FW05A = Arc(source=m.fs.fwh5_da.outlet, destination=m.fs.bfp.inlet) m.fs.EXTR_BFPT_A = Arc(source=m.fs.turb.ip_split[10].outlet_3, destination=m.fs.bfpt.inlet) m.fs.EXHST_BFPT = Arc(source=m.fs.bfpt.outlet, destination=m.fs.condenser_mix.bfpt) ############################################################################ # High-pressure feedwater heaters # ############################################################################ # fwh6 m.fs.FW05B = Arc(source=m.fs.bfp.outlet, destination=m.fs.fwh6.cooling.inlet_2) m.fs.FWH6_DRN = Arc(source=m.fs.fwh6.cooling.outlet_1, destination=m.fs.fwh5_da.drain) m.fs.EXTR_IP5 = Arc(source=m.fs.turb.ip_split[5].outlet_2, destination=m.fs.fwh6.desuperheat.inlet_1) # fwh7 m.fs.FW06 = Arc(source=m.fs.fwh6.desuperheat.outlet_2, destination=m.fs.fwh7.cooling.inlet_2) m.fs.FWH7_DRN = Arc(source=m.fs.fwh7.cooling.outlet_1, destination=m.fs.fwh6.drain_mix.drain) m.fs.EXTR_HP7 = Arc(source=m.fs.turb.hp_split[7].outlet_2, destination=m.fs.fwh7.desuperheat.inlet_1) # fwh8 m.fs.FW07 = Arc(source=m.fs.fwh7.desuperheat.outlet_2, destination=m.fs.fwh8.cooling.inlet_2) m.fs.FWH8_DRN = Arc(source=m.fs.fwh8.cooling.outlet_1, destination=m.fs.fwh7.drain_mix.drain) m.fs.EXTR_HP4 = Arc(source=m.fs.turb.hp_split[4].outlet_2, destination=m.fs.fwh8.desuperheat.inlet_1) ############################################################################ # Turn the Arcs into constraints and return the model # ############################################################################ pyo.TransformationFactory("network.expand_arcs").apply_to(m.fs) return m
def create_model( steady_state=True, time_set=[0,3], nfe=5, calc_integ=True, form=PIDForm.standard ): """ Create a test model and solver Args: steady_state (bool): If True, create a steady state model, otherwise create a dynamic model time_set (list): The begining and end point of the time domain nfe (int): Number of finite elements argument for the DAE transformation calc_integ (bool): If Ture, calculate in the initial condition for the integral term, else use a fixed variable (fs.ctrl.err_i0), flase is the better option if you have a value from a previous time period form: whether the equations are written in the standard or velocity form Returns (tuple): (ConcreteModel, Solver) """ if steady_state: fs_cfg = {"dynamic":False} model_name = "Steam Tank, Steady State" else: fs_cfg = {"dynamic":True, "time_set":time_set} model_name = "Steam Tank, Dynamic" m = pyo.ConcreteModel(name=model_name) m.fs = FlowsheetBlock(default=fs_cfg) # Create a property parameter block m.fs.prop_water = iapws95.Iapws95ParameterBlock( default={"phase_presentation":iapws95.PhaseType.LG}) # Create the valve and tank models m.fs.valve_1 = SteamValve(default={ "dynamic":False, "has_holdup":False, "material_balance_type":MaterialBalanceType.componentTotal, "property_package":m.fs.prop_water}) m.fs.tank = Heater(default={ "has_holdup":True, "material_balance_type":MaterialBalanceType.componentTotal, "property_package":m.fs.prop_water}) m.fs.valve_2 = SteamValve(default={ "dynamic":False, "has_holdup":False, "material_balance_type":MaterialBalanceType.componentTotal, "property_package":m.fs.prop_water}) # Connect the models m.fs.v1_to_t = Arc(source=m.fs.valve_1.outlet, destination=m.fs.tank.inlet) m.fs.t_to_v2 = Arc(source=m.fs.tank.outlet, destination=m.fs.valve_2.inlet) # The control volume block doesn't assume the two phases are in equilibrium # by default, so I'll make that assumption here, I don't actually expect # liquid to form but who knows. The phase_fraction in the control volume is # volumetric phase fraction hence the densities. @m.fs.tank.Constraint(m.fs.time) def vol_frac_vap(b, t): return b.control_volume.properties_out[t].phase_frac["Vap"]\ *b.control_volume.properties_out[t].dens_mol\ /b.control_volume.properties_out[t].dens_mol_phase["Vap"]\ == b.control_volume.phase_fraction[t, "Vap"] # Add the stream constraints and do the DAE transformation pyo.TransformationFactory('network.expand_arcs').apply_to(m.fs) if not steady_state: pyo.TransformationFactory('dae.finite_difference').apply_to( m.fs, nfe=nfe, wrt=m.fs.time, scheme='BACKWARD') # Fix the derivative variables to zero at time 0 (steady state assumption) m.fs.fix_initial_conditions() # A tank pressure reference that's directly time-indexed m.fs.tank_pressure = pyo.Reference( m.fs.tank.control_volume.properties_out[:].pressure) # Add a controller m.fs.ctrl = PIDBlock(default={"pv":m.fs.tank_pressure, "output":m.fs.valve_1.valve_opening, "upper":1.0, "lower":0.0, "calculate_initial_integral":calc_integ, "pid_form":form}) m.fs.ctrl.deactivate() # Don't want controller turned on by default # Fix the input variables m.fs.valve_1.inlet.enth_mol.fix(50000) m.fs.valve_1.inlet.pressure.fix(5e5) m.fs.valve_2.outlet.pressure.fix(101325) m.fs.valve_1.Cv.fix(0.001) m.fs.valve_2.Cv.fix(0.001) m.fs.valve_1.valve_opening.fix(1) m.fs.valve_2.valve_opening.fix(1) m.fs.tank.heat_duty.fix(0) m.fs.tank.control_volume.volume.fix(2.0) m.fs.ctrl.gain.fix(1e-6) m.fs.ctrl.time_i.fix(0.1) m.fs.ctrl.time_d.fix(0.1) m.fs.ctrl.setpoint.fix(3e5) # Initialize the model solver = pyo.SolverFactory("ipopt") solver.options = {'tol': 1e-6, 'linear_solver': "ma27", 'max_iter': 100} for t in m.fs.time: m.fs.valve_1.inlet.flow_mol = 100 # initial guess on flow # simple initialize m.fs.valve_1.initialize(outlvl=1) _set_port(m.fs.tank.inlet, m.fs.valve_1.outlet) m.fs.tank.initialize(outlvl=1) _set_port(m.fs.valve_2.inlet, m.fs.tank.outlet) m.fs.valve_2.initialize(outlvl=1) solver.solve(m, tee=True) # Return the model and solver return m, solver
def main(): """ """ nxfe = 10 # NOTE: Default inputs: (128.2, 591.4) ic_inputs = { #"fs.MB.gas_phase.properties[*,0.0].flow_mol": 120.0, #"fs.MB.solid_phase.properties[*,1.0].flow_mass": 550.0, } ic_model_params = {"nxfe": nxfe} m_ic = get_steady_state_model( ic_inputs, solve_kwds={"tee": True}, model_params=ic_model_params, ) time = m_ic.fs.time scalar_data, dae_data = get_data_from_steady_model(m_ic, time) # TODO: get steady state data (scalar and dae both necessary) x0 = 0.0 x1 = 1.0 # These (as well as nxfe above) are the parameters for a small model # that I'm using to test NMPC (defaults are 900, 15, 60), nxfe=10 horizon = 1800 tfe_width = 60 sample_width = 120 sample_points = [ # Calculate sample points first with integer arithmetic # to avoid roundoff error float(sample_width*i) for i in range(0, horizon//sample_width + 1) ] horizon = float(horizon) tfe_width = float(tfe_width) model_params = { "horizon": horizon, "tfe_width": tfe_width, "ntcp": 1, "nxfe": nxfe, } # These are approximately the default values: #disturbance_dict = {"CO2": 0.03, "H2O": 0.0, "CH4": 0.97} disturbance_dict = {"CO2": 0.5, "H2O": 0.0, "CH4": 0.5} disturbance = dict( ( "fs.MB.gas_phase.properties[*,%s].mole_frac_comp[%s]" % (x0, j), {(0.0, horizon): val}, ) for j, val in disturbance_dict.items() ) # Create solver here as it is needed to solve for the setpoint solver = pyo.SolverFactory("ipopt") solver.options["linear_solver"] = "ma57" solver.options["max_cpu_time"] = 1500 # # Get setpoint data # sp_inputs = get_inputs_at_time(disturbance, horizon) #sp_inputs.update({ # "fs.MB.gas_phase.properties[*,0.0].flow_mol": 272.8, # "fs.MB.solid_phase.properties[*,1.0].flow_mass": 591.4, #}) sp_model_params = {"nxfe": nxfe} m_sp = get_steady_state_model( sp_inputs, solve_kwds={"tee": True}, model_params=sp_model_params, ) time = m_sp.fs.time space = m_sp.fs.MB.gas_phase.length_domain t0 = time.first() # Solve optimization problem for setpoint sp_objective_states = [ "fs.MB.solid_phase.reactions[*,%s].OC_conv" % x0, ] sp_target = { "fs.MB.solid_phase.reactions[*,%s].OC_conv" % x0: 0.95, } m_sp.fs.MB.gas_inlet.flow_mol[:].unfix() m_sp.setpoint_expr = get_tracking_cost_expressions( sp_objective_states, time, sp_target ) m_sp.objective = pyo.Objective(expr=m_sp.setpoint_expr[t0]) solver.solve(m_sp, tee=True) scalar_vars, dae_vars = flatten_dae_components(m_sp, time, pyo.Var) setpoint = { str(pyo.ComponentUID(var.referent)): var[t0].value for var in dae_vars } ### max_data = get_max_values_from_steady(m_sp) variance_data = get_variance_of_time_slices(m_sp, time, space) #weight_data = None weight_data = { name: 1.0/s if s != 0 else 1.0 for name, s in variance_data.items() #name: 1/w if w != 0 else 1.0 for name, w in max_data.items() # Note: 1/w**2 does not converge with states in objective... } objective_states = get_state_variable_names(space) flattened_vars = [None, None] m = get_model_for_dynamic_optimization( sample_points=sample_points, parameter_perturbation=disturbance, model_params=model_params, ic_scalar_data=scalar_data, ic_dae_data=dae_data, setpoint_data=setpoint, objective_weights=weight_data, objective_states=objective_states, # this argument is a huge hack to get the flattened # vars without having to do a bit more work. flatten_out=flattened_vars, ) add_constraints_for_missing_variables(m) time = m.fs.time t0 = time.first() scalar_vars, dae_vars = flattened_vars initialize_dynamic(m, dae_vars) # Should we initialize to setpoint inputs? #sp_input_dict = { # "fs.MB.gas_phase.properties[*,0.0].flow_mol": {(t0, horizon): 250.0}, # "fs.MB.solid_phase.properties[*,1.0].flow_mass": {(t0, horizon): 591.4}, #} #load_inputs_into_model(m, time, sp_input_dict) # TODO: Should I set inlet flow rates to their target values for # this simulation? input_vardata = ( [m.fs.MB.gas_inlet.flow_mol[t] for t in time if t != t0] + [m.fs.MB.solid_inlet.flow_mass[t] for t in time if t != t0] ) with TemporarySubsystemManager( to_fix=input_vardata, to_deactivate=[m.piecewise_constant_constraint], ): print("Initializing by time element...") with TIMER.context("elem-init"): initialize_by_time_element(m, time, solver) m.fs.MB.solid_phase.reactions[:,0.0].OC_conv.setlb(0.89) print("Starting dynamic optimization solve...") with TIMER.context("solve dynamic"): solver.solve(m, tee=True) extra_states = [ pyo.Reference(m.fs.MB.solid_phase.reactions[:,0.0].OC_conv), ] plot_outlet_states_over_time(m, show=False, extra_states=extra_states) inputs = [ "fs.MB.gas_phase.properties[*,0.0].flow_mol", "fs.MB.solid_phase.properties[*,1.0].flow_mass", ] plot_inputs_over_time(m, inputs, show=False) print(m.tracking_cost.name) for t in m.fs.time: print(t, pyo.value(m.tracking_cost[t])) print()