def test_add_setpoint_to_controller(self): # This test just creates an objective then makes sure it has the # correct variables. More detailed tests will be performed for # construct_objective_weights and add_objective_function. nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() namespace = getattr(nmpc.controller, nmpc.namespace_name) setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) categories = [VariableCategory.DIFFERENTIAL] nmpc.add_setpoint_to_controller(objective_state_categories=categories) assert hasattr(namespace, 'tracking_objective') obj_variables = ComponentSet( identify_variables(namespace.tracking_objective.expr)) n_samples = len(namespace.sample_points) - 1 assert len(obj_variables) == (len(namespace.diff_vars) + len(namespace.input_vars)) * n_samples for var in namespace.diff_vars.varlist + namespace.input_vars.varlist: for ts in namespace.sample_points: if ts == t0: continue assert var[ts] in obj_variables
def test_calculate_full_state_setpoint(self): # come up with setpoint, solve, check data structures, # make sure values of setpoint make sense. nmpc = self.make_nmpc() namespace = getattr(nmpc.controller, nmpc.namespace_name) t0 = nmpc.controller_time.first() setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) categories = [ VariableCategory.DIFFERENTIAL, VariableCategory.ALGEBRAIC ] for categ in categories: group = namespace.category_dict[categ] for i, var in enumerate(group): assert group.setpoint[i] is not None assert namespace.diff_vars.setpoint[0] == pytest.approx(3.75) assert namespace.diff_vars.setpoint[1] == pytest.approx(1.25) assert namespace.alg_vars.setpoint[0] == pytest.approx(3.0) assert namespace.alg_vars.setpoint[1] == pytest.approx(-3.75) assert namespace.alg_vars.setpoint[2] == pytest.approx(3.75) assert namespace.input_vars.setpoint[0] == pytest.approx(3.0)
def test_initialize_by_solving_elements(self): # Make nmpc # add setpoint to controller # add pwc_constraints # call initialize_by_solving_elements # assert that values are as expected. nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() tl = time.last() controller = nmpc.controller namespace = getattr(controller, nmpc.namespace_name) setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) copy_values_forward(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) override = [ (nmpc.controller.conc[t0, 'A'], 1), (nmpc.controller.conc[t0, 'B'], 1), (nmpc.controller.rate[t0, 'A'], 1), (nmpc.controller.rate[t0, 'B'], 1), (nmpc.controller.flow_out[t0], 1), (nmpc.controller.flow_in[t0], 1), ] nmpc.add_setpoint_to_controller(objective_weight_override=override) nmpc.constrain_control_inputs_piecewise_constant() nmpc.controller.flow_in[:].set_value(2.0) nmpc.initialize_by_solving_elements( controller, time, input_type=ElementInitializationInputOption.INITIAL, ) input_vars = namespace.input_vars diff_vars = namespace.diff_vars deriv_vars = namespace.deriv_vars assert input_vars[0][tl].value == 2.0 assert diff_vars[0][tl].value == pytest.approx(3.185595567867036) assert diff_vars[1][tl].value == pytest.approx(1.1532474073395755) assert deriv_vars[0][tl].value == pytest.approx(0.44321329639889284) assert deriv_vars[1][tl].value == pytest.approx(0.8791007531878847) nmpc.initialize_by_solving_elements( controller, time, input_type=ElementInitializationInputOption.SET_POINT, ) for t in time: if t != t0: assert input_vars[0][t] == 3.0 assert diff_vars[0][tl].value == pytest.approx(3.7037037037037037) assert diff_vars[1][tl].value == pytest.approx(1.0746896480968502) assert deriv_vars[0][tl].value == pytest.approx(0.1851851851851849) assert deriv_vars[1][tl].value == pytest.approx(0.47963475941315314)
def test_construct_objective_weights(self): """ Populates setpoint values, """ nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() namespace = getattr(nmpc.controller, nmpc.namespace_name) setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) reference = namespace.diff_vars.reference setpoint = namespace.diff_vars.setpoint n_diff_vars = len(namespace.diff_vars) predicted_diff_weights = [ 1. / abs(reference[i] - setpoint[i]) for i in range(n_diff_vars) ] tol = 1e-5 nmpc.construct_objective_weights( nmpc.controller, categories=[ VariableCategory.DIFFERENTIAL, VariableCategory.INPUT, ], objective_weight_tolerance=tol, ) for pred, act in zip(predicted_diff_weights, namespace.diff_vars.weights): assert pred == act assert namespace.input_vars.weights[0] == 1 / tol override = [ (nmpc.controller.rate[t0, 'A'], 2), (nmpc.controller.rate[t0, 'B'], 2), (nmpc.controller.flow_out[t0], 2), ] nmpc.construct_objective_weights( nmpc.controller, categories=[ VariableCategory.ALGEBRAIC, ], objective_weight_override=override, ) for w in namespace.alg_vars.weights: assert w == 2
def test_solve_control_problem(self): # make nmpc # add setpoint # initialize, from ICs probably # solve control problem # assert values are as expected. # Could probably even do this without initialization nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() tl = time.last() controller = nmpc.controller namespace = getattr(controller, nmpc.namespace_name) t1 = namespace.sample_points[1] setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. # One way to deal with this pitfall is to add bounds to inlet flow. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) copy_values_forward(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) override = [ (nmpc.controller.conc[t0, 'A'], 1), (nmpc.controller.conc[t0, 'B'], 1), (nmpc.controller.rate[t0, 'A'], 1), (nmpc.controller.rate[t0, 'B'], 1), (nmpc.controller.flow_out[t0], 1), (nmpc.controller.flow_in[t0], 1), ] nmpc.add_setpoint_to_controller(objective_weight_override=override) nmpc.constrain_control_inputs_piecewise_constant() nmpc.solve_control_problem() input_vars = namespace.input_vars diff_vars = namespace.diff_vars assert input_vars[0][t1].value == pytest.approx(3.192261151432352) assert input_vars[0][tl].value == pytest.approx(2.9818775607191648) assert diff_vars[0][tl].value == pytest.approx(3.7101450012850137) assert diff_vars[1][tl].value == pytest.approx(1.0898406680173942) assert nmpc.controller_solved
def test_initialize_control_problem(self): # Each initialization strategy should be tested in more detail # in other functions. # This function should test that each strategy can be passed to # the user-facing function, and that the values left in the # controller model are somewhat reasonable nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() namespace = getattr(nmpc.controller, nmpc.namespace_name) setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) copy_values_forward(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) override = [ (nmpc.controller.conc[t0, 'A'], 1), (nmpc.controller.conc[t0, 'B'], 1), (nmpc.controller.rate[t0, 'A'], 1), (nmpc.controller.rate[t0, 'B'], 1), (nmpc.controller.flow_out[t0], 1), (nmpc.controller.flow_in[t0], 1), ] nmpc.add_setpoint_to_controller(objective_weight_override=override) nmpc.constrain_control_inputs_piecewise_constant() nmpc.initialize_control_problem( control_init_option=ControlInitOption.FROM_INITIAL_CONDITIONS) nmpc.initialize_control_problem( control_init_option=ControlInitOption.BY_TIME_ELEMENT) with pytest.raises(RuntimeError): nmpc.initialize_control_problem( control_init_option=ControlInitOption.FROM_PREVIOUS)
def test_initialize_from_previous_sample(self): nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() tl = time.last() controller = nmpc.controller namespace = getattr(controller, nmpc.namespace_name) sample_points = namespace.sample_points t1 = sample_points[1] setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. # One way to deal with this pitfall is to add bounds to inlet flow. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) copy_values_forward(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) override = [ (nmpc.controller.conc[t0, 'A'], 1), (nmpc.controller.conc[t0, 'B'], 1), (nmpc.controller.rate[t0, 'A'], 1), (nmpc.controller.rate[t0, 'B'], 1), (nmpc.controller.flow_out[t0], 1), (nmpc.controller.flow_in[t0], 1), ] nmpc.add_setpoint_to_controller(objective_weight_override=override) nmpc.constrain_control_inputs_piecewise_constant() nmpc.solve_control_problem() diff_vars = namespace.diff_vars input_vars = namespace.input_vars sample_time = nmpc.sample_time n_samples = namespace.samples_per_horizon cat_dict = namespace.category_dict categories = [ VariableCategory.DIFFERENTIAL, VariableCategory.INPUT, ] expected = { categ: { s: [{ t - sample_time: cat_dict[categ][i][t].value for t in time if sample_points[s] < t and t <= sample_points[s + 1] } for i in range(len(cat_dict[categ]))] for s in range(1, n_samples) } for categ in categories } for categ in categories: expected[categ][n_samples] = [{ t: cat_dict[categ].setpoint[i] for t in time if sample_points[n_samples - 1] < t and t <= sample_points[n_samples] } for i in range(len(cat_dict[categ]))] nmpc.initialize_from_previous_sample(nmpc.controller) for s in range(1, n_samples): interval = [ t for t in time if sample_points[s - 1] < t and t <= sample_points[s] ] for categ in categories: for i, var in enumerate(cat_dict[categ]): for t in interval: assert var[t].value == expected[categ][s][i][t]
def test_add_objective_function(self): # populate setpoints, # construct weights (with override everywhere) # add objective function # compare objective function to prediction # for diff+error, diff+action, diff+alg+action setpoints nmpc = self.make_nmpc() time = nmpc.controller_time t0 = time.first() namespace = getattr(nmpc.controller, nmpc.namespace_name) setpoint = [ (nmpc.controller.flow_in[t0], 3.0), ] # Note: without this initialization, the setpoint arrives at a # local optimum with inlet flow ~= 0. nmpc.controller.flow_in[:].set_value(3.0) initialize_t0(nmpc.controller) copy_values_forward(nmpc.controller) nmpc.calculate_full_state_setpoint(setpoint, outlvl=idaeslog.DEBUG) override = [ (nmpc.controller.conc[t0, 'A'], 1), (nmpc.controller.conc[t0, 'B'], 1), (nmpc.controller.rate[t0, 'A'], 1), (nmpc.controller.rate[t0, 'B'], 1), (nmpc.controller.flow_out[t0], 1), (nmpc.controller.flow_in[t0], 1), ] nmpc.construct_objective_weights( nmpc.controller, objective_weight_override=override, ) pred_obj = {i: 0. for i in range(1, 4)} sample_points = namespace.sample_points[1:] setpoint = namespace.diff_vars.setpoint for i, v in enumerate(namespace.diff_vars): for t in sample_points: pred_obj[1] += (v[t] - setpoint[i])**2 pred_obj[2] += (v[t] - setpoint[i])**2 pred_obj[3] += (v[t] - setpoint[i])**2 setpoint = namespace.alg_vars.setpoint for i, v in enumerate(namespace.alg_vars): for t in sample_points: pred_obj[3] += (v[t] - setpoint[i])**2 setpoint = namespace.input_vars.setpoint for i, v in enumerate(namespace.input_vars): for t in sample_points: t_prev = t - nmpc.sample_time pred_obj[1] += (v[t] - setpoint[i])**2 pred_obj[2] += (v[t] - v[t_prev])**2 pred_obj[3] += (v[t] - v[t_prev])**2 nmpc.add_objective_function( nmpc.controller, control_penalty_type=ControlPenaltyType.ERROR, objective_state_categories=[ VariableCategory.DIFFERENTIAL, ]) assert aml.value(namespace.objective.expr == pred_obj[1]) namespace.del_component(namespace.objective) nmpc.add_objective_function( nmpc.controller, control_penalty_type=ControlPenaltyType.ACTION, objective_state_categories=[ VariableCategory.DIFFERENTIAL, ]) assert aml.value(namespace.objective.expr == pred_obj[2]) namespace.del_component(namespace.objective) nmpc.add_objective_function( nmpc.controller, control_penalty_type=ControlPenaltyType.ACTION, objective_state_categories=[ VariableCategory.DIFFERENTIAL, VariableCategory.ALGEBRAIC, ]) assert aml.value(namespace.objective.expr == pred_obj[3]) namespace.del_component(namespace.objective)
def test_calculate_full_state_setpoint(nmpc): controller = nmpc.controller controller.mixer.E_inlet.flow_vol[0].fix(0.1) controller.mixer.S_inlet.flow_vol[0].fix(2.0) nmpc.solve_consistent_initial_conditions(controller) assert nmpc.has_consistent_initial_conditions(controller, tolerance=1e-6) # Deactivate tracking objective from previous tests #controller._NMPC_NAMESPACE.tracking_objective.deactivate() 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.2), (controller.mixer.S_inlet.flow_vol[0], 2.5)] weight_tolerance = 5e-7 weight_override = [ (controller.mixer.E_inlet.flow_vol[0.], 20.), (controller.mixer.S_inlet.flow_vol[0.], 2.), (controller.cstr.control_volume.energy_holdup[0., 'aq'], 0.1), (controller.cstr.outlet.conc_mol[0., 'P'], 1.), (controller.cstr.outlet.conc_mol[0., 'S'], 1.), ] # FIXME: This steady state setpoint solve is more sensitive than I # would like. nmpc.calculate_full_state_setpoint(set_point, objective_weight_tolerance=weight_tolerance, objective_weight_override=weight_override) assert hasattr(controller._NMPC_NAMESPACE, 'user_setpoint') user_setpoint = controller._NMPC_NAMESPACE.user_setpoint assert hasattr(controller._NMPC_NAMESPACE, 'user_setpoint_weights') user_setpoint_weights = controller._NMPC_NAMESPACE.user_setpoint_weights assert hasattr(controller._NMPC_NAMESPACE, 'user_setpoint_vars') user_setpoint_vars = controller._NMPC_NAMESPACE.user_setpoint_vars for i, var in enumerate(user_setpoint_vars): # if var.local_name.startswith('conc'): # assert user_setpoint_weights[i] == 1. # elif var.local_name.startswith('energy'): # assert user_setpoint_weights[i] == 0.1 if var.local_name.startswith('E_'): assert user_setpoint_weights[i] == 20. elif var.local_name.startswith('S_'): assert user_setpoint_weights[i] == 2. alg_vars = controller._NMPC_NAMESPACE.alg_vars diff_vars = controller._NMPC_NAMESPACE.diff_vars input_vars = controller._NMPC_NAMESPACE.input_vars categories = [ VariableCategory.DIFFERENTIAL, VariableCategory.ALGEBRAIC, VariableCategory.DERIVATIVE, VariableCategory.INPUT, ] category_dict = controller._NMPC_NAMESPACE.category_dict for categ in categories: group = category_dict[categ] # Assert that setpoint has been populated with non-None values assert not any([sp is None for sp in group.setpoint]) # Assert that setpoint (target) and reference (initial) values are # different in some way assert not all([sp == ref for sp, ref in zip(group.setpoint, group.reference)]) # Assert that initial and reference values are the same assert all([ref == var[0].value for ref, var in zip(group.reference, group.varlist)])