def test_constructor_4(): m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2) sample_time = 0.5 # Six samples per horizon, five elements per sample initial_plant_inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0] ] # Fix some derivative vars, as in pseudo-steady state # Controller model only for t in m_controller.fs.time: m_controller.fs.cstr.control_volume.\ energy_accumulation[t, 'aq'].fix(0) m_controller.fs.cstr.control_volume.\ material_accumulation[t, 'aq', 'E'].fix(0) m_controller.fs.cstr.control_volume.\ energy_holdup[0, 'aq'].unfix() m_controller.fs.cstr.control_volume.\ material_holdup[0, 'aq', 'E'].unfix() m_controller.fs.cstr.control_volume.\ energy_accumulation_disc_eq.deactivate() m_controller.fs.cstr.control_volume.\ material_accumulation_disc_eq.deactivate() nmpc = NMPCSim(m_plant.fs, m_plant.fs.time, m_controller.fs, m_controller.fs.time, inputs_at_t0=initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time)
def test_constructor_3(): m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2) sample_time = 0.5 # Six samples per horizon, five elements per sample initial_plant_inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0] ] # energy_holdup should now be a fixed variable # (only in plant model...) m_plant.fs.cstr.control_volume.energy_holdup.fix(300) m_plant.fs.cstr.control_volume.energy_accumulation_disc_eq.deactivate() nmpc = NMPCSim(m_plant.fs, m_plant.fs.time, m_controller.fs, m_controller.fs.time, inputs_at_t0=initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time) assert_categorization(m_plant.fs)
def test_constructor_1(): m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2) sample_time = 0.5 # Six samples per horizon, five elements per sample initial_plant_inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0] ] nmpc = NMPCSim(m_plant.fs, m_plant.fs.time, m_controller.fs, m_controller.fs.time, initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time) assert hasattr(nmpc, 'plant') assert hasattr(nmpc, 'controller') assert hasattr(nmpc, 'sample_time') plant = nmpc.plant controller = nmpc.controller assert hasattr(plant._NMPC_NAMESPACE, 'diff_vars') assert hasattr(plant._NMPC_NAMESPACE, 'deriv_vars') assert hasattr(plant._NMPC_NAMESPACE, 'alg_vars') assert hasattr(plant._NMPC_NAMESPACE, 'input_vars') assert hasattr(plant._NMPC_NAMESPACE, 'fixed_vars') assert hasattr(plant._NMPC_NAMESPACE, 'scalar_vars') assert hasattr(plant._NMPC_NAMESPACE, 'ic_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_diff_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_input_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_alg_vars') assert hasattr(controller._NMPC_NAMESPACE, 'diff_vars') assert hasattr(controller._NMPC_NAMESPACE, 'deriv_vars') assert hasattr(controller._NMPC_NAMESPACE, 'alg_vars') assert hasattr(controller._NMPC_NAMESPACE, 'input_vars') assert hasattr(controller._NMPC_NAMESPACE, 'fixed_vars') assert hasattr(controller._NMPC_NAMESPACE, 'scalar_vars') assert hasattr(controller._NMPC_NAMESPACE, 'ic_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_diff_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_input_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_alg_vars') assert_categorization(controller) assert_categorization(plant)
def test_categorize_error(): model = make_model(horizon=1, ntfe=5, ntcp=2) time = model.fs.time scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) # Add a dummy var to treat as an input. # This var is not in `dae_vars`, so it will not be located during # categorization, which should fail. model.dummy_var = Var(time) init_input_list = [ model.fs.mixer.S_inlet.flow_vol[0], model.fs.mixer.E_inlet.flow_vol[0], model.dummy_var[0], ] with pytest.raises(RuntimeError, match=r'Not all inputs could be found'): category_dict = categorize_dae_variables( dae_vars, time, init_input_list, ) # Re-run flattener. Now `dummy_var` should be included in `dae_vars`. scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) category_dict = categorize_dae_variables( dae_vars, time, init_input_list, )
def test_constructor_2(): m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2) sample_time = 0.5 # Six samples per horizon, five elements per sample initial_plant_inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0] ] # Change some initial conditions - in controller model only # Specify temperature instead of energy holdup m_controller.fs.cstr.control_volume.energy_holdup\ [m_controller.fs.time.first(), 'aq'].unfix() m_controller.fs.cstr.outlet.temperature[0].fix(300) # Specify C_P instead of holdup m_controller.fs.cstr.control_volume.material_holdup\ [m_controller.fs.time.first(), 'aq', 'P'].unfix() m_controller.fs.cstr.outlet.conc_mol[m_controller.fs.time.first(), 'P'].fix(0) # specify \dot M_C insteady of M_C m_controller.fs.cstr.control_volume.material_holdup\ [m_controller.fs.time.first(), 'aq', 'C'].unfix() m_controller.fs.cstr.control_volume.material_accumulation\ [m_controller.fs.time.first(), 'aq', 'C'].fix(0) nmpc = NMPCSim(m_plant.fs, m_plant.fs.time, m_controller.fs, m_controller.fs.time, inputs_at_t0=initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time) # IPOPT output looks a little weird solving for initial conditions here... # has non-zero dual infeasibility, iteration 1 has a non-zero # regularization coefficient. (Would love to debug this with a # transparent NLP solver...) # Check that variables have been categorized properly assert_categorization(m_controller.fs)
def test_initialize_by_element_in_range(): mod = make_model(horizon=2, ntfe=20) assert degrees_of_freedom(mod) == 0 scalar_vars, dae_vars = flatten_dae_variables(mod.fs, mod.fs.time) diff_vars = [ Reference(mod.fs.cstr.control_volume.energy_holdup[:, 'aq']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'S']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'E']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'C']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'P']) ] initialize_by_element_in_range(mod.fs, mod.fs.time, 0, 1, solver=solver, dae_vars=dae_vars, time_linking_variables=diff_vars, outlvl=idaeslog.DEBUG, solve_initial_conditions=True) assert degrees_of_freedom(mod.fs) == 0 assert mod.fs.cstr.outlet.conc_mol[1, 'S'].value == approx(10.189, abs=1e-3) assert mod.fs.cstr.outlet.conc_mol[1, 'C'].value == approx(0.4275, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[1, 'E'].value == approx(0.0541, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[1, 'P'].value == approx(0.3503, abs=1e-4) initialize_by_element_in_range(mod.fs, mod.fs.time, 1, 2, solver=solver, dae_vars=dae_vars, outlvl=idaeslog.DEBUG) assert degrees_of_freedom(mod.fs) == 0 for con in activated_equalities_generator(mod.fs): assert value(con.body) - value(con.upper) < 1e-5 assert mod.fs.cstr.outlet.conc_mol[2, 'S'].value == approx(11.263, abs=1e-3) assert mod.fs.cstr.outlet.conc_mol[2, 'C'].value == approx(0.4809, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[2, 'E'].value == approx(0.0538, abs=1e-4) assert mod.fs.cstr.outlet.conc_mol[2, 'P'].value == approx(0.4372, abs=1e-4)
def steady_cstr_model(): return make_model(steady=True).fs
def nmpc(): # This tests the same model constructed in the test_nmpc_constructor_1 file m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2, bounds=True) sample_time = 0.5 # Six samples per horizon, five elements per sample initial_plant_inputs = [m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0]] nmpc = NMPCSim(m_plant.fs, m_plant.fs.time, m_controller.fs, m_controller.fs.time, inputs_at_t0=initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time) # IPOPT output looks a little weird solving for initial conditions here... # has non-zero dual infeasibility, iteration 1 has a non-zero # regularization coefficient. (Would love to debug this with a # transparent NLP solver...) assert hasattr(nmpc, 'plant') assert hasattr(nmpc, 'plant_time') assert hasattr(nmpc, 'controller') assert hasattr(nmpc, 'controller_time') assert hasattr(nmpc, 'sample_time') plant = nmpc.plant p_time = nmpc.plant_time controller = nmpc.controller c_time = nmpc.controller_time assert hasattr(plant, '_NMPC_NAMESPACE') assert hasattr(controller, '_NMPC_NAMESPACE') assert hasattr(plant._NMPC_NAMESPACE, 'diff_vars') assert hasattr(plant._NMPC_NAMESPACE, 'deriv_vars') assert hasattr(plant._NMPC_NAMESPACE, 'alg_vars') assert hasattr(plant._NMPC_NAMESPACE, 'input_vars') assert hasattr(plant._NMPC_NAMESPACE, 'fixed_vars') assert hasattr(plant._NMPC_NAMESPACE, 'scalar_vars') assert hasattr(plant._NMPC_NAMESPACE, 'ic_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_diff_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_deriv_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_input_vars') assert hasattr(plant._NMPC_NAMESPACE, 'n_alg_vars') assert hasattr(controller._NMPC_NAMESPACE, 'diff_vars') assert hasattr(controller._NMPC_NAMESPACE, 'deriv_vars') assert hasattr(controller._NMPC_NAMESPACE, 'alg_vars') assert hasattr(controller._NMPC_NAMESPACE, 'input_vars') assert hasattr(controller._NMPC_NAMESPACE, 'fixed_vars') assert hasattr(controller._NMPC_NAMESPACE, 'scalar_vars') assert hasattr(controller._NMPC_NAMESPACE, 'ic_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_diff_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_deriv_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_input_vars') assert hasattr(controller._NMPC_NAMESPACE, 'n_alg_vars') # Check that variables have been categorized properly assert_categorization(controller) assert_categorization(plant) return nmpc
def test_categorize_4(): """ This tests categorization when a psuedo-steady state approximation is used. Energy accumulation and accumulation of E are fixed, the corresponding initial conditions unfixed, and the corresponding discretization equations deactivated. """ model = make_model(horizon=1, ntfe=5, ntcp=2) time = model.fs.time CV = model.fs.cstr.control_volume CV.energy_accumulation[:, 'aq'].fix(0.) CV.material_accumulation[:, 'aq', 'E'].fix(0.) CV.energy_holdup[0, 'aq'].unfix() CV.material_holdup[0, 'aq', 'E'].unfix() CV.energy_accumulation_disc_eq.deactivate() CV.material_accumulation_disc_eq.deactivate() init_input_list = [ model.fs.mixer.S_inlet.flow_vol[0], model.fs.mixer.E_inlet.flow_vol[0], ] init_input_set = ComponentSet(init_input_list) init_deriv_list = [ CV.material_accumulation[0, 'aq', 'C'], CV.material_accumulation[0, 'aq', 'S'], CV.material_accumulation[0, 'aq', 'P'], CV.material_accumulation[0, 'aq', 'Solvent'], ] init_deriv_set = ComponentSet(init_deriv_list) init_diff_list = [ CV.material_holdup[0, 'aq', 'C'], CV.material_holdup[0, 'aq', 'S'], CV.material_holdup[0, 'aq', 'P'], CV.material_holdup[0, 'aq', 'Solvent'], ] init_diff_set = ComponentSet(init_diff_list) init_fixed_list = [ model.fs.cstr.control_volume.energy_accumulation[0, 'aq'], CV.material_accumulation[0, 'aq', 'E'], model.fs.mixer.E_inlet.temperature[0], model.fs.mixer.S_inlet.temperature[0], *list(model.fs.mixer.E_inlet.conc_mol[0, :]), *list(model.fs.mixer.S_inlet.conc_mol[0, :]), model.fs.cstr.outlet.conc_mol[0, 'Solvent'], model.fs.mixer.outlet.conc_mol[0, 'Solvent'], ] init_fixed_set = ComponentSet(init_fixed_list) init_meas_list = [ model.fs.cstr.control_volume.volume[0], CV.material_holdup[0, 'aq', 'P'], CV.material_holdup[0, 'aq', 'C'], CV.material_holdup[0, 'aq', 'S'], ] init_meas_set = ComponentSet(init_meas_list) init_alg_list = [ model.fs.cstr.control_volume.energy_holdup[0, 'aq'], CV.material_holdup[0, 'aq', 'E'], model.fs.cstr.outlet.flow_vol[0], model.fs.cstr.outlet.temperature[0], model.fs.cstr.inlet.flow_vol[0], model.fs.cstr.inlet.temperature[0], model.fs.mixer.outlet.flow_vol[0], model.fs.mixer.outlet.temperature[0], model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.properties_out[0].flow_mol_comp[:]), *list(model.fs.cstr.inlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.properties_in[0].flow_mol_comp[:]), *list(model.fs.cstr.control_volume.rate_reaction_generation[0, 'aq', :]), *list(model.fs.mixer.mixed_state[0].flow_mol_comp[:]), *list(model.fs.mixer.E_inlet_state[0].flow_mol_comp[:]), *list(model.fs.mixer.S_inlet_state[0].flow_mol_comp[:]), *list(model.fs.cstr.outlet.conc_mol[0, :]), *list(model.fs.mixer.outlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.reactions[0].reaction_coef[:]), *list(model.fs.cstr.control_volume.reactions[0].reaction_rate[:]), *list(model.fs.cstr.control_volume.rate_reaction_extent[0, :]), ] init_alg_set = ComponentSet(init_alg_list) # solvent outlet concentrations are not algebraic variables as # it is fixed. init_alg_set.remove(model.fs.cstr.outlet.conc_mol[0, 'Solvent']) init_alg_set.remove(model.fs.mixer.outlet.conc_mol[0, 'Solvent']) scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) category_dict = categorize_dae_variables(dae_vars, time, init_input_list) input_vars = category_dict[VC.INPUT] diff_vars = category_dict[VC.DIFFERENTIAL] deriv_vars = category_dict[VC.DERIVATIVE] fixed_vars = category_dict[VC.FIXED] alg_vars = category_dict[VC.ALGEBRAIC] meas_vars = category_dict[VC.MEASUREMENT] assert len(input_vars) == len(init_input_set) for v in input_vars: assert v[0] in init_input_set assert len(deriv_vars) == len(init_deriv_set) for v in deriv_vars: assert v[0] in init_deriv_set assert len(diff_vars) == len(init_deriv_set) for v in diff_vars: assert v[0] in init_diff_set assert len(fixed_vars) == len(init_fixed_set) for v in fixed_vars: assert v[0] in init_fixed_set assert len(alg_vars) == len(init_alg_set) for v in alg_vars: assert v[0] in init_alg_set assert len(meas_vars) == len(init_meas_set) for v in meas_vars: assert v[0] in init_meas_set assert len(scalar_vars) == 0
def main(plot_switch=False): # This tests the same model constructed in the test_nmpc_constructor_1 file m_plant = make_model(horizon=6, ntfe=60, ntcp=2) m_controller = make_model(horizon=3, ntfe=30, ntcp=2, bounds=True) sample_time = 0.5 time_plant = m_plant.fs.time n_plant_samples = (time_plant.last() - time_plant.first()) / sample_time assert n_plant_samples == int(n_plant_samples) n_plant_samples = int(n_plant_samples) plant_sample_points = [ time_plant.first() + i * sample_time for i in range(1, n_plant_samples) ] for t in plant_sample_points: assert t in time_plant # Six samples per horizon, five elements per sample initial_plant_inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0] ] nmpc = NMPCSim(plant_model=m_plant.fs, plant_time_set=m_plant.fs.time, controller_model=m_controller.fs, controller_time_set=m_controller.fs.time, inputs_at_t0=initial_plant_inputs, solver=solver, outlvl=idaeslog.DEBUG, sample_time=sample_time) plant = nmpc.plant controller = nmpc.controller nmpc.solve_consistent_initial_conditions(plant) nmpc.solve_consistent_initial_conditions(controller) set_point = [(controller.cstr.outlet.conc_mol[0, 'P'], 0.4), (controller.cstr.outlet.conc_mol[0, 'S'], 0.0), (controller.cstr.control_volume.energy_holdup[0, 'aq'], 300), (controller.mixer.E_inlet.flow_vol[0], 0.1), (controller.mixer.S_inlet.flow_vol[0], 2.0)] # Interestingly, this (st.st. set point) solve converges infeasible # if energy_holdup set point is not 300. (Needs higher weight?) weight_tolerance = 5e-7 # Weight overwrite expects a list of (VarData, value) tuples # in the STEADY MODEL weight_override = [(controller.mixer.E_inlet.flow_vol[0], 20.0)] nmpc.calculate_full_state_setpoint( set_point, objective_weight_override=weight_override, objective_weight_tolerance=weight_tolerance, outlvl=idaeslog.DEBUG, allow_inconsistent=False, tolerance=1e-6) nmpc.add_setpoint_to_controller() nmpc.constrain_control_inputs_piecewise_constant() nmpc.initialize_control_problem( control_init_option=ControlInitOption.FROM_INITIAL_CONDITIONS) nmpc.solve_control_problem() nmpc.inject_control_inputs_into_plant(time_plant.first(), add_input_noise=True) nmpc.simulate_plant(time_plant.first()) for t in plant_sample_points: nmpc.transfer_current_plant_state_to_controller(t, add_plant_noise=True) nmpc.initialize_control_problem( control_init_option=ControlInitOption.FROM_PREVIOUS) nmpc.solve_control_problem() nmpc.inject_control_inputs_into_plant(t, add_input_noise=True) nmpc.simulate_plant(t) # TODO: add option for specifying "user-interest variables" if plot_switch: temp_info = plant._NMPC_NAMESPACE.var_locator[ plant.cstr.outlet.temperature[0.]] temp_location = temp_info.location temp_group = temp_info.group temperature_data = PlotData(temp_group, temp_location, name='Temperature') fig, ax = temperature_data.plot() fig.savefig(temperature_data.name) P_info = plant._NMPC_NAMESPACE.var_locator[plant.cstr.outlet.conc_mol[ 0., 'P']] P_location = P_info.location P_group = P_info.group P_data = PlotData(P_group, P_location, name='P_conc') fig, ax = P_data.plot() fig.savefig(P_data.name) S_info = plant._NMPC_NAMESPACE.var_locator[plant.cstr.outlet.conc_mol[ 0., 'S']] S_location = S_info.location S_group = S_info.group S_data = PlotData(S_group, S_location, name='S_conc') fig, ax = S_data.plot() fig.savefig(S_data.name)
def test_categorize_3(): """ In this test we fix one of the differential variables. This is the case where somebody wants to run an isothermal CSTR. """ model = make_model(horizon=1, ntfe=5, ntcp=2) time = model.fs.time CV = model.fs.cstr.control_volume CV.energy_holdup.fix(300) CV.energy_accumulation_disc_eq.deactivate() init_input_list = [ model.fs.mixer.S_inlet.flow_vol[0], model.fs.mixer.E_inlet.flow_vol[0], ] init_input_set = ComponentSet(init_input_list) init_deriv_list = [ *list(model.fs.cstr.control_volume.material_accumulation[0, 'aq', :]), ] init_deriv_set = ComponentSet(init_deriv_list) init_diff_list = [ *list(model.fs.cstr.control_volume.material_holdup[0, 'aq', :]), ] init_diff_set = ComponentSet(init_diff_list) init_fixed_list = [ # Energy holdup has been fixed model.fs.cstr.control_volume.energy_holdup[0, 'aq'], model.fs.mixer.E_inlet.temperature[0], model.fs.mixer.S_inlet.temperature[0], *list(model.fs.mixer.E_inlet.conc_mol[0, :]), *list(model.fs.mixer.S_inlet.conc_mol[0, :]), model.fs.cstr.outlet.conc_mol[0, 'Solvent'], model.fs.mixer.outlet.conc_mol[0, 'Solvent'], ] init_fixed_set = ComponentSet(init_fixed_list) init_meas_list = [ model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.material_holdup[0, 'aq', :]), ] init_meas_set = ComponentSet(init_meas_list) # Solvent holdup is not a measurement; we measure volume instead init_meas_set.remove( model.fs.cstr.control_volume.material_holdup[0, 'aq', 'Solvent']) init_alg_list = [ # Since energy_holdup is fixed, energy_accumulation is not # considered a derivative. Instead it is algebraic. model.fs.cstr.control_volume.energy_accumulation[0, 'aq'], model.fs.cstr.outlet.flow_vol[0], model.fs.cstr.outlet.temperature[0], model.fs.cstr.inlet.flow_vol[0], model.fs.cstr.inlet.temperature[0], model.fs.mixer.outlet.flow_vol[0], model.fs.mixer.outlet.temperature[0], model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.properties_out[0].flow_mol_comp[:]), *list(model.fs.cstr.inlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.properties_in[0].flow_mol_comp[:]), *list(model.fs.cstr.control_volume.rate_reaction_generation[0, 'aq', :]), *list(model.fs.mixer.mixed_state[0].flow_mol_comp[:]), *list(model.fs.mixer.E_inlet_state[0].flow_mol_comp[:]), *list(model.fs.mixer.S_inlet_state[0].flow_mol_comp[:]), *list(model.fs.cstr.outlet.conc_mol[0, :]), *list(model.fs.mixer.outlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.reactions[0].reaction_coef[:]), *list(model.fs.cstr.control_volume.reactions[0].reaction_rate[:]), *list(model.fs.cstr.control_volume.rate_reaction_extent[0, :]), ] init_alg_set = ComponentSet(init_alg_list) # solvent outlet concentrations are not algebraic variables as # it is fixed. init_alg_set.remove(model.fs.cstr.outlet.conc_mol[0, 'Solvent']) init_alg_set.remove(model.fs.mixer.outlet.conc_mol[0, 'Solvent']) scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) category_dict = categorize_dae_variables(dae_vars, time, init_input_list) input_vars = category_dict[VC.INPUT] diff_vars = category_dict[VC.DIFFERENTIAL] deriv_vars = category_dict[VC.DERIVATIVE] fixed_vars = category_dict[VC.FIXED] alg_vars = category_dict[VC.ALGEBRAIC] meas_vars = category_dict[VC.MEASUREMENT] assert len(input_vars) == len(init_input_set) for v in input_vars: assert v[0] in init_input_set assert len(deriv_vars) == len(init_deriv_set) for v in deriv_vars: assert v[0] in init_deriv_set assert len(diff_vars) == len(init_deriv_set) for v in diff_vars: assert v[0] in init_diff_set assert len(fixed_vars) == len(init_fixed_set) for v in fixed_vars: assert v[0] in init_fixed_set assert len(alg_vars) == len(init_alg_set) for v in alg_vars: assert v[0] in init_alg_set assert len(meas_vars) == len(init_meas_set) for v in meas_vars: assert v[0] in init_meas_set assert len(scalar_vars) == 0
def test_categorize_1(): """ This test categorizes the enzyme cstr "as intended." Volume is used as a measurement, and solvent flow rates are fixed, but otherwise equations are as expected. """ model = make_model(horizon=1, ntfe=5, ntcp=2) time = model.fs.time init_input_list = [ model.fs.mixer.S_inlet.flow_vol[0], model.fs.mixer.E_inlet.flow_vol[0], ] init_input_set = ComponentSet(init_input_list) init_deriv_list = [ model.fs.cstr.control_volume.energy_accumulation[0, 'aq'], *list(model.fs.cstr.control_volume.material_accumulation[0, 'aq', :]), ] init_deriv_set = ComponentSet(init_deriv_list) init_diff_list = [ model.fs.cstr.control_volume.energy_holdup[0, 'aq'], *list(model.fs.cstr.control_volume.material_holdup[0, 'aq', :]), ] init_diff_set = ComponentSet(init_diff_list) init_fixed_list = [ model.fs.mixer.E_inlet.temperature[0], model.fs.mixer.S_inlet.temperature[0], *list(model.fs.mixer.E_inlet.conc_mol[0, :]), *list(model.fs.mixer.S_inlet.conc_mol[0, :]), model.fs.cstr.outlet.conc_mol[0, 'Solvent'], model.fs.mixer.outlet.conc_mol[0, 'Solvent'], ] init_fixed_set = ComponentSet(init_fixed_list) init_meas_list = [ model.fs.cstr.control_volume.energy_holdup[0, 'aq'], model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.material_holdup[0, 'aq', :]), ] init_meas_set = ComponentSet(init_meas_list) # Solvent holdup is not a measurement; we measure volume instead init_meas_set.remove( model.fs.cstr.control_volume.material_holdup[0, 'aq', 'Solvent']) init_alg_list = [ model.fs.cstr.outlet.flow_vol[0], model.fs.cstr.outlet.temperature[0], model.fs.cstr.inlet.flow_vol[0], model.fs.cstr.inlet.temperature[0], model.fs.mixer.outlet.flow_vol[0], model.fs.mixer.outlet.temperature[0], model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.properties_out[0].flow_mol_comp[:]), *list(model.fs.cstr.inlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.properties_in[0].flow_mol_comp[:]), *list(model.fs.cstr.control_volume.rate_reaction_generation[0, 'aq', :]), *list(model.fs.mixer.mixed_state[0].flow_mol_comp[:]), *list(model.fs.mixer.E_inlet_state[0].flow_mol_comp[:]), *list(model.fs.mixer.S_inlet_state[0].flow_mol_comp[:]), *list(model.fs.cstr.outlet.conc_mol[0, :]), *list(model.fs.mixer.outlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.reactions[0].reaction_coef[:]), *list(model.fs.cstr.control_volume.reactions[0].reaction_rate[:]), *list(model.fs.cstr.control_volume.rate_reaction_extent[0, :]), ] init_alg_set = ComponentSet(init_alg_list) # solvent outlet concentrations are not algebraic variables as # it is fixed. init_alg_set.remove(model.fs.cstr.outlet.conc_mol[0, 'Solvent']) init_alg_set.remove(model.fs.mixer.outlet.conc_mol[0, 'Solvent']) scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) category_dict = categorize_dae_variables(dae_vars, time, init_input_list) input_vars = category_dict[VC.INPUT] diff_vars = category_dict[VC.DIFFERENTIAL] deriv_vars = category_dict[VC.DERIVATIVE] fixed_vars = category_dict[VC.FIXED] alg_vars = category_dict[VC.ALGEBRAIC] meas_vars = category_dict[VC.MEASUREMENT] assert len(input_vars) == len(init_input_set) for v in input_vars: assert v[0] in init_input_set assert len(deriv_vars) == len(init_deriv_set) for v in deriv_vars: assert v[0] in init_deriv_set assert len(diff_vars) == len(init_deriv_set) for v in diff_vars: assert v[0] in init_diff_set assert len(fixed_vars) == len(init_fixed_set) for v in fixed_vars: assert v[0] in init_fixed_set assert len(alg_vars) == len(init_alg_set) for v in alg_vars: assert v[0] in init_alg_set assert len(meas_vars) == len(init_meas_set) for v in meas_vars: assert v[0] in init_meas_set assert len(scalar_vars) == 0
def test_categorize_2(): """ In this instance, temperature is "measured" (used as an initial condition) instead of energy_holdup, conc[P] is measured instead of holdup[P], and accumulation[C] is measured instead of holdup[C]. """ model = make_model(horizon=1, ntfe=5, ntcp=2) time = model.fs.time t0 = time.first() material_holdup = model.fs.cstr.control_volume.material_holdup material_accumulation = model.fs.cstr.control_volume.material_accumulation energy_holdup = model.fs.cstr.control_volume.energy_holdup energy_accumulation = model.fs.cstr.control_volume.energy_accumulation conc_mol = model.fs.cstr.outlet.conc_mol # Specify temperature instead of energy holdup energy_holdup[t0, 'aq'].unfix() model.fs.cstr.outlet.temperature[t0].fix(300) # Specify C_P instead of holdup material_holdup[t0, 'aq', 'P'].unfix() conc_mol[t0, 'P'].fix(0.) # Specify accumulation of C instead of holdup # You might want to do this if you want to start at steady state, # but don't know the value of every variable at the steady state # you want to start at... material_holdup[t0, 'aq', 'C'].unfix() material_accumulation[t0, 'aq', 'C'].fix(0.) init_input_list = [ model.fs.mixer.S_inlet.flow_vol[0], model.fs.mixer.E_inlet.flow_vol[0], ] init_input_set = ComponentSet(init_input_list) init_deriv_list = [ model.fs.cstr.control_volume.energy_accumulation[0, 'aq'], *list(model.fs.cstr.control_volume.material_accumulation[0, 'aq', :]), ] init_deriv_set = ComponentSet(init_deriv_list) init_diff_list = [ model.fs.cstr.control_volume.energy_holdup[0, 'aq'], *list(model.fs.cstr.control_volume.material_holdup[0, 'aq', :]), ] init_diff_set = ComponentSet(init_diff_list) init_fixed_list = [ model.fs.mixer.E_inlet.temperature[0], model.fs.mixer.S_inlet.temperature[0], *list(model.fs.mixer.E_inlet.conc_mol[0, :]), *list(model.fs.mixer.S_inlet.conc_mol[0, :]), model.fs.cstr.outlet.conc_mol[0, 'Solvent'], model.fs.mixer.outlet.conc_mol[0, 'Solvent'], ] init_fixed_set = ComponentSet(init_fixed_list) init_meas_list = [ model.fs.cstr.outlet.temperature[0], model.fs.cstr.control_volume.volume[0], model.fs.cstr.control_volume.material_holdup[0, 'aq', 'S'], model.fs.cstr.control_volume.material_holdup[0, 'aq', 'E'], model.fs.cstr.control_volume.material_holdup[0, 'aq', 'S'], model.fs.cstr.control_volume.material_accumulation[0, 'aq', 'C'], model.fs.cstr.outlet.conc_mol[0, 'P'], ] init_meas_set = ComponentSet(init_meas_list) # No need to remove solvent holdup here as it was not added to this list. init_alg_list = [ model.fs.cstr.outlet.flow_vol[0], model.fs.cstr.outlet.temperature[0], model.fs.cstr.inlet.flow_vol[0], model.fs.cstr.inlet.temperature[0], model.fs.mixer.outlet.flow_vol[0], model.fs.mixer.outlet.temperature[0], model.fs.cstr.control_volume.volume[0], *list(model.fs.cstr.control_volume.properties_out[0].flow_mol_comp[:]), *list(model.fs.cstr.inlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.properties_in[0].flow_mol_comp[:]), *list(model.fs.cstr.control_volume.rate_reaction_generation[0, 'aq', :]), *list(model.fs.mixer.mixed_state[0].flow_mol_comp[:]), *list(model.fs.mixer.E_inlet_state[0].flow_mol_comp[:]), *list(model.fs.mixer.S_inlet_state[0].flow_mol_comp[:]), *list(model.fs.cstr.outlet.conc_mol[0, :]), *list(model.fs.mixer.outlet.conc_mol[0, :]), *list(model.fs.cstr.control_volume.reactions[0].reaction_coef[:]), *list(model.fs.cstr.control_volume.reactions[0].reaction_rate[:]), *list(model.fs.cstr.control_volume.rate_reaction_extent[0, :]), ] init_alg_set = ComponentSet(init_alg_list) # solvent outlet concentrations are not algebraic variables as # it is fixed. init_alg_set.remove(model.fs.cstr.outlet.conc_mol[0, 'Solvent']) init_alg_set.remove(model.fs.mixer.outlet.conc_mol[0, 'Solvent']) scalar_vars, dae_vars = flatten_dae_components(model, time, ctype=Var) category_dict = categorize_dae_variables(dae_vars, time, init_input_list) input_vars = category_dict[VC.INPUT] diff_vars = category_dict[VC.DIFFERENTIAL] deriv_vars = category_dict[VC.DERIVATIVE] fixed_vars = category_dict[VC.FIXED] alg_vars = category_dict[VC.ALGEBRAIC] meas_vars = category_dict[VC.MEASUREMENT] assert len(input_vars) == len(init_input_set) for v in input_vars: assert v[0] in init_input_set assert len(deriv_vars) == len(init_deriv_set) for v in deriv_vars: assert v[0] in init_deriv_set assert len(diff_vars) == len(init_deriv_set) for v in diff_vars: assert v[0] in init_diff_set assert len(fixed_vars) == len(init_fixed_set) for v in fixed_vars: assert v[0] in init_fixed_set assert len(alg_vars) == len(init_alg_set) for v in alg_vars: assert v[0] in init_alg_set assert len(meas_vars) == len(init_meas_set) for v in meas_vars: assert v[0] in init_meas_set assert len(scalar_vars) == 0
def test_add_noise_at_time(): mod = make_model(horizon=2, ntfe=20) time = mod.fs.time t0 = time.first() assert degrees_of_freedom(mod) == 0 scalar_vars, dae_vars = flatten_dae_variables(mod.fs, time) diff_vars = [ Reference(mod.fs.cstr.control_volume.energy_holdup[:, 'aq']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'S']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'E']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'C']), Reference(mod.fs.cstr.control_volume.material_holdup[:, 'aq', 'P']) ] for t in time: diff_vars[0][t].setlb(290) diff_vars[0][t].setub(310) for i in range(1, 5): diff_vars[i][t].setlb(0) diff_vars[i][t].setub(1) # Pretend this is mole fraction... assert diff_vars[0][0].value == 300 for i in range(1, 5): assert diff_vars[i][0].value == 0.001 copy_values_at_time(diff_vars, diff_vars, [t for t in time if t != t0], t0) for seed in [4, 8, 15, 16, 23, 42]: random.seed(seed) weights = [10, 0.001, 0.001, 0.001, 0.001] nom_vals = add_noise_at_time(diff_vars, 0, weights=weights) assert nom_vals[0][0] == 300 assert diff_vars[0][0].value != 300 assert diff_vars[0][0].value == approx(300, abs=2) for i in range(1, 5): assert nom_vals[0][i] == 0.001 # ^ nom_vals indexed by time, then var-index. This is confusing, # might need to change (or only accept one time point at a time) assert diff_vars[i][0].value != 0.001 assert diff_vars[i][0].value == approx(0.001, abs=2e-4) # Within four standard deviations should be a safe check for i in range(0, 5): diff_vars[i][0].set_value(nom_vals[0][i]) # Reset and try again with new seed # Try providing function for uniform random rand_fcn = random.uniform random_arg_dict = { 'range_list': [(295, 305), (0.001, 0.01), (0.001, 0.01), (0.001, 0.01), (0.001, 0.01)] } def args_fcn(i, val, **kwargs): # args_fcn expects arguments like this range_list = kwargs.pop('range_list', None) return range_list[i] nom_vals = add_noise_at_time(diff_vars, 0.5, random_function=rand_fcn, args_function=args_fcn, random_arg_dict=random_arg_dict) assert nom_vals[0.5][0] == 300 assert diff_vars[0][0.5].value != 300 assert 295 <= diff_vars[0][0.5].value <= 305 for i in range(1, 5): assert nom_vals[0.5][i] == 0.001 assert diff_vars[i][0.5].value != 0.001 assert 0.001 <= diff_vars[i][0.5].value <= 0.01 # Try to get some bound violations random_arg_dict = { 'range_list': [(295, 305), (1, 2), (1, 2), (1, 2), (1, 2)] } nom_vals = add_noise_at_time(diff_vars, 1, random_function=rand_fcn, args_function=args_fcn, random_arg_dict=random_arg_dict, bound_strategy='push', bound_push=0.01) for i in range(1, 5): assert diff_vars[i][1].value == 0.99 random.seed(123) with pytest.raises(ValueError) as exc_test: # Large weights - one of these lower bounds should fail... nom_vals = add_noise_at_time(diff_vars, 1.5, bound_strategy='discard', discard_limit=0, weights=[1, 1, 1, 1, 1], sig_0=0.05) @pytest.mark.unit def test_get_violated_bounds_at_time(): m = ConcreteModel() m.time = Set(initialize=[1, 2, 3]) m.v = Var(m.time, ['a', 'b', 'c'], initialize=5) varlist = [ Reference(m.v[:, 'a']), Reference(m.v[:, 'b']), Reference(m.v[:, 'c']) ] group = NMPCVarGroup(varlist, m.time) group.set_lb(0, 0) group.set_lb(1, 6) group.set_lb(2, 0) group.set_ub(0, 4) group.set_ub(1, 10) group.set_ub(2, 10) violated = get_violated_bounds_at_time(group, [1, 2, 3], tolerance=1e-8) violated_set = ComponentSet(violated) for t in m.time: assert m.v[t, 'a'] in violated_set assert m.v[t, 'b'] in violated_set violated = get_violated_bounds_at_time(group, 2, tolerance=1e-8) violated_set = ComponentSet(violated) assert m.v[2, 'a'] in violated_set assert m.v[2, 'b'] in violated_set
def dynamic_cstr_model(): return make_model().fs
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 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 simulation_horizon = 60 n_samples_to_simulate = round(simulation_horizon / sample_time) samples_to_simulate = [ time_plant.first() + i * sample_time for i in range(1, n_samples_to_simulate) ] # We must identify for the controller which variables are our # inputs and measurements. inputs = [ m_plant.fs.mixer.S_inlet.flow_vol[0], m_plant.fs.mixer.E_inlet.flow_vol[0], ] measurements = [ m_controller.fs.cstr.outlet.conc_mol[0, 'C'], m_controller.fs.cstr.outlet.conc_mol[0, 'E'], m_controller.fs.cstr.outlet.conc_mol[0, 'S'], m_controller.fs.cstr.outlet.conc_mol[0, 'P'], m_controller.fs.cstr.outlet.temperature[0], m_controller.fs.cstr.volume[0], ] # Construct the "NMPC simulator" object nmpc = NMPCSim( plant_model=m_plant, plant_time_set=m_plant.fs.time, controller_model=m_controller, controller_time_set=m_controller.fs.time, inputs_at_t0=inputs, measurements=measurements, sample_time=sample_time, ) plant = nmpc.plant controller = nmpc.controller p_t0 = nmpc.plant.time.first() c_t0 = nmpc.controller.time.first() p_ts = nmpc.plant.sample_points[1] c_ts = nmpc.controller.sample_points[1] solve_consistent_initial_conditions(plant, plant.time, solver) solve_consistent_initial_conditions(controller, controller.time, solver) # 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.0), (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.) nmpc.controller.mod.fs.cstr.control_volume.material_holdup[0, ...].unfix() nmpc.controller.mod.fs.cstr.control_volume.energy_holdup[0, ...].unfix() nmpc.controller.mod.fs.cstr.volume[0].unfix() nmpc.controller.add_setpoint_objective(setpoint, setpoint_weights) nmpc.controller.solve_setpoint(solver) # Now we are ready to construct the tracking NMPC problem tracking_weights = [ *((v, 1.) for v in nmpc.controller.vectors.differential[:, 0]), *((v, 1.) for v in nmpc.controller.vectors.input[:, 0]), ] nmpc.controller.add_tracking_objective(tracking_weights) nmpc.controller.constrain_control_inputs_piecewise_constant() nmpc.controller.initialize_to_initial_conditions() # Solve the first control problem nmpc.controller.vectors.input[...].unfix() nmpc.controller.vectors.input[:, 0].fix() solver.solve(nmpc.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 = nmpc.controller.mod.fs.cstr variance = [ (cstr.outlet.conc_mol[0.0, 'S'], 0.2), (cstr.outlet.conc_mol[0.0, 'E'], 0.05), (cstr.outlet.conc_mol[0.0, 'C'], 0.1), (cstr.outlet.conc_mol[0.0, 'P'], 0.05), (cstr.outlet.temperature[0.0], 5.), (cstr.volume[0.0], 0.05), ] nmpc.controller.set_variance(variance) measurement_variance = [v.variance for v in controller.measurement_vars] measurement_noise_bounds = [(0.0, var[c_t0].ub) for var in controller.measurement_vars] 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), ] nmpc.plant.set_variance(variance) input_variance = [v.variance for v in plant.input_vars] input_noise_bounds = [(0.0, var[p_t0].ub) for var in plant.input_vars] random.seed(246) # 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. nmpc.plant.initialize_by_solving_elements(solver) solver.solve(nmpc.plant) for i in range(1, 11): print('\nENTERING NMPC LOOP ITERATION %s\n' % i) measured = nmpc.plant.generate_measurements_at_time(p_ts) nmpc.plant.advance_one_sample() nmpc.plant.initialize_to_initial_conditions() measured = apply_noise_with_bounds( measured, measurement_variance, random.gauss, measurement_noise_bounds, ) nmpc.controller.advance_one_sample() nmpc.controller.load_measurements(measured) solver.solve(nmpc.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) nmpc.plant.initialize_by_solving_elements(solver) solver.solve(nmpc.plant)