def make_block(self, sample_time=0.5, horizon=1, nfe=2): model = make_model(horizon=horizon, nfe=nfe) time = model.time t0 = time.first() inputs = [model.flow_in[t0]] measurements = [model.conc[t0, 'A'], model.conc[t0, 'B']] scalar_vars, dae_vars = flatten_dae_components( model, time, pyo.Var, ) category_dict = categorize_dae_variables(dae_vars, time, inputs, measurements=measurements) dyn_block = DynamicBlock( model=model, time=time, category_dict={None: category_dict}, #inputs=inputs, #measurements=measurements, ) dyn_block.construct() dyn_block.set_sample_time(sample_time) return dyn_block
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_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 make_block(self, sample_time=0.5, horizon=1, nfe=2): model = make_model(horizon=horizon, nfe=nfe) time = model.time t0 = time.first() inputs = [model.flow_in[t0]] measurements = [model.conc[t0,'A'], model.conc[t0,'B']] dyn_block = DynamicBlock( model=model, time=time, inputs=inputs, measurements=measurements, ) dyn_block.construct() dyn_block.set_sample_time(sample_time) return dyn_block
def test_set_sample_time(self): model = make_model(horizon=1, nfe=2) time = model.time t0 = time.first() inputs = [model.flow_in[t0]] measurements = [model.conc[t0,'A'], model.conc[t0,'B']] blk = DynamicBlock( model=model, time=time, inputs=inputs, measurements=measurements, ) blk.construct() blk.set_sample_time(1.0) assert blk.sample_points == [0.0, 1.0] blk.set_sample_time(0.5) assert blk.sample_points == [0.0, 0.5, 1.0]
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)
class NMPCSim(object): """ This is a user-facing class to perform NMPC simulations with Pyomo models for both plant and controller. The user must provide the models to use for each, along with sets to treat as "time," inputs in the plant model, and measurements in the controller model. Its functionality is primarily to ensure that these components (as defined by the names relative to the corresponding provided models) exist on both models. """ # TODO: pyomo.common.config.add_docstring_list def __init__(self, plant_model=None, plant_time_set=None, controller_model=None, controller_time_set=None, inputs_at_t0=None, measurements=None, sample_time=None, **kwargs): """ Measurements must be defined in the controller model. Inputs must be defined in the plant model. """ # To find components in a model given a name, # modulo the index of some set: # i. slice the component along the set # ii. create a cuid from that slice # iii. get a reference to the slice from the cuid on the new model # iv. access the reference at the index you want (optional) self.measurement_cuids = [ ComponentUID( slice_component_along_sets(comp, (controller_time_set, ))) for comp in measurements ] self.input_cuids = [ ComponentUID(slice_component_along_sets(comp, (plant_time_set, ))) for comp in inputs_at_t0 ] p_t0 = plant_time_set.first() init_plant_measurements = [ cuid.find_component_on(plant_model)[p_t0] for cuid in self.measurement_cuids ] self.plant = DynamicBlock( model=plant_model, time=plant_time_set, inputs=inputs_at_t0, measurements=init_plant_measurements, ) self.plant.construct() # Here we repeat essentially the same "find component" # procedure as above. c_t0 = controller_time_set.first() init_controller_inputs = [ cuid.find_component_on(controller_model)[c_t0] for cuid in self.input_cuids ] self.controller = ControllerBlock( model=controller_model, time=controller_time_set, inputs=init_controller_inputs, measurements=measurements, ) self.controller.construct() if sample_time is not None: self.controller.set_sample_time(sample_time) self.plant.set_sample_time(sample_time) self.sample_time = sample_time
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()