def test_generate_subsystems_with_fixed_var(self): m = _make_simple_model() m.v4.fix() subs = [ ([m.con1], [m.v1]), ([m.con2, m.con3], [m.v2, m.v3]), ] other_vars = [ [m.v2, m.v3], [m.v1], ] for i, (block, inputs) in enumerate(generate_subsystem_blocks(subs)): inputs = list(block.input_vars.values()) with TemporarySubsystemManager(to_fix=inputs): self.assertIs(block.model(), block) var_set = ComponentSet(subs[i][1]) con_set = ComponentSet(subs[i][0]) input_set = ComponentSet(other_vars[i]) self.assertEqual(len(var_set), len(block.vars)) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(input_set), len(inputs)) self.assertTrue(all(var in var_set for var in block.vars[:])) self.assertTrue(all(con in con_set for con in block.cons[:])) self.assertTrue(all(var in input_set for var in inputs)) self.assertTrue(all(var.fixed for var in inputs)) self.assertFalse(any(var.fixed for var in block.vars[:])) # Test that we have properly unfixed variables, except variables # that were already fixed. self.assertFalse(m.v1.fixed) self.assertFalse(m.v2.fixed) self.assertFalse(m.v3.fixed) self.assertTrue(m.v4.fixed)
def test_fix_then_solve(self): # This is a test of the expected use case. We have a (square) # subsystem that we can solve easily after fixing and deactivating # certain variables and constraints. m = _make_simple_model() ipopt = pyo.SolverFactory("ipopt") # Initialize to avoid converging infeasible due to bad pivots m.v1.set_value(1.0) m.v2.set_value(1.0) m.v3.set_value(1.0) m.v4.set_value(2.0) with TemporarySubsystemManager(to_fix=[m.v3, m.v4], to_deactivate=[m.con1]): # Solve the subsystem with m.v1, m.v2 unfixed and # m.con2, m.con3 inactive. ipopt.solve(m) # Have solved model to expected values self.assertAlmostEqual(m.v1.value, pyo.sqrt(7.0), delta=1e-8) self.assertAlmostEqual(m.v2.value, pyo.sqrt(4.0 - pyo.sqrt(7.0)), delta=1e-8)
def initialize_by_time_element( m, time, solver, solve_kwds=None, skip_partition=False, flatten_vars=None, flatten_cons=None, time_subsystems=None, ): if solve_kwds is None: solve_kwds = {} reslist = [] for block, inputs in generate_time_element_blocks( m, time, skip_partition=skip_partition, flatten_vars=flatten_vars, flatten_cons=flatten_cons, time_subsystems=time_subsystems, ): with TemporarySubsystemManager(to_fix=inputs): with TIMER.context("solve"): res = solver.solve(block, **solve_kwds) reslist.append(res) return reslist
def test_generate_subsystems_include_fixed_var(self): m = _make_simple_model() m.v4.fix() subsystems = [ ([m.con1], [m.v1]), ([m.con2, m.con3], [m.v2, m.v3]), ] other_vars = [ [m.v2, m.v3, m.v4], [m.v1, m.v4], ] for i, (block, inputs) in enumerate(generate_subsystem_blocks( subsystems, include_fixed=True, )): with TemporarySubsystemManager(to_fix=inputs): self.assertIs(block.model(), block) var_set = ComponentSet(subsystems[i][1]) con_set = ComponentSet(subsystems[i][0]) input_set = ComponentSet(other_vars[i]) self.assertEqual(len(var_set), len(block.vars)) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(input_set), len(block.input_vars)) self.assertTrue(all(var in var_set for var in block.vars[:])) self.assertTrue(all(con in con_set for con in block.cons[:])) self.assertTrue(all(var in input_set for var in inputs)) self.assertTrue(all(var.fixed for var in inputs)) self.assertFalse(any(var.fixed for var in block.vars[:])) self.assertFalse(m.v1.fixed) self.assertFalse(m.v2.fixed) self.assertFalse(m.v3.fixed) self.assertTrue(m.v4.fixed)
def test_context_some_redundant(self): m = _make_simple_model() to_fix = [m.v2, m.v4] to_deactivate = [m.con1, m.con2] to_reset = [m.v1] m.v1.set_value(1.5) m.v2.fix() m.con1.deactivate() with TemporarySubsystemManager(to_fix, to_deactivate, to_reset): self.assertEqual(m.v1.value, 1.5) self.assertTrue(m.v2.fixed) self.assertTrue(m.v4.fixed) self.assertFalse(m.con1.active) self.assertFalse(m.con2.active) m.v1.set_value(2.0) m.v2.set_value(3.0) self.assertEqual(m.v1.value, 1.5) self.assertEqual(m.v2.value, 3.0) self.assertTrue(m.v2.fixed) self.assertFalse(m.v4.fixed) self.assertTrue(m.con2.active) self.assertFalse(m.con1.active)
def test_generate_subsystems_without_fixed_var(self): m = _make_simple_model() subs = [ ([m.con1], [m.v1, m.v4]), ([m.con2, m.con3], [m.v2, m.v3]), ] other_vars = [ [m.v2, m.v3], [m.v1, m.v4], ] for i, (block, inputs) in enumerate(generate_subsystem_blocks(subs)): with TemporarySubsystemManager(to_fix=inputs): self.assertIs(block.model(), block) var_set = ComponentSet(subs[i][1]) con_set = ComponentSet(subs[i][0]) input_set = ComponentSet(other_vars[i]) self.assertEqual(len(var_set), len(block.vars)) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(input_set), len(block.input_vars)) self.assertTrue(all(var in var_set for var in block.vars[:])) self.assertTrue(all(con in con_set for con in block.cons[:])) self.assertTrue(all(var in input_set for var in inputs)) self.assertTrue(all(var.fixed for var in inputs)) self.assertFalse(any(var.fixed for var in block.vars[:])) # Test that we have properly unfixed variables self.assertFalse(any(var.fixed for var in m.component_data_objects(pyo.Var)))
def get_polynomial_degree_wrt(constraints, variables=None): # TODO: Move this function elsewhere from pyomo.core.expr.visitor import polynomial_degree from pyomo.util.subsystems import TemporarySubsystemManager vars_in_cons = list(_generate_variables_in_constraints(constraints)) if variables is None: variables = vars_in_cons var_set = ComponentSet(variables) other_vars = [v for v in vars_in_cons if v not in var_set] with TemporarySubsystemManager(to_fix=other_vars): con_poly_degree = [polynomial_degree(con.expr) for con in constraints] if any(d is None for d in con_poly_degree): # General nonlinear return None else: return max(con_poly_degree)
def initialize_steady(m): """ This is my approach for initializing the steady state model. Set state variables to their inlet values, except gas temperature, which is initialized to the temperature of the solid inlet. Then deactivate discretization equations and strongly connected component decomposition. """ gas_inlet_names = set_gas_values_to_inlets(m) solid_inlet_names = set_solid_values_to_inlets(m) set_gas_temperature_to_solid_inlet(m) gas_phase = m.fs.MB.gas_phase solid_phase = m.fs.MB.solid_phase gas_length = m.fs.MB.gas_phase.length_domain solid_length = m.fs.MB.solid_phase.length_domain gas_disc_eqs = [ con for _, con, _ in generate_discretization_components_along_set( m, gas_length) ] solid_disc_eqs = [ con for _, con, _ in generate_discretization_components_along_set( m, solid_length) ] gas_sum_eqn_slice = gas_phase.properties[:, :].sum_component_eqn gas_sum_eqn_slice.attribute_errors_generate_exceptions = False solid_sum_eqn_slice = solid_phase.properties[:, :].sum_component_eqn solid_sum_eqn_slice.attribute_errors_generate_exceptions = False to_deactivate = [] to_deactivate.extend(gas_sum_eqn_slice) to_deactivate.extend(solid_sum_eqn_slice) to_deactivate.extend(gas_disc_eqs) to_deactivate.extend(solid_disc_eqs) to_fix = [] for name in gas_inlet_names + solid_inlet_names: ref = m.find_component(name) to_fix.extend(ref.values()) with TemporarySubsystemManager(to_fix=to_fix, to_deactivate=to_deactivate): solve_strongly_connected_components(m)
def test_generate_subsystems_with_exception(self): m = _make_simple_model() subsystems = [ ([m.con1], [m.v1, m.v4]), ([m.con2, m.con3], [m.v2, m.v3]), ] other_vars = [ [m.v2, m.v3], [m.v1, m.v4], ] block = create_subsystem_block(*subsystems[0]) with self.assertRaises(RuntimeError): inputs = list(block.input_vars[:]) with TemporarySubsystemManager(to_fix=inputs): self.assertTrue(all(var.fixed for var in inputs)) self.assertFalse(any(var.fixed for var in block.vars[:])) raise RuntimeError() # Test that we have properly unfixed variables self.assertFalse(any(var.fixed for var in m.component_data_objects(pyo.Var)))
def test_with_external_function(self): m = self._make_model_with_external_functions() subsystem = ([m.con2, m.con3], [m.v2, m.v3]) m.v1.set_value(0.5) block = create_subsystem_block(*subsystem) ipopt = pyo.SolverFactory("ipopt") with TemporarySubsystemManager(to_fix=list(block.input_vars.values())): ipopt.solve(block) # Correct values obtained by solving with Ipopt directly # in another script. self.assertEqual(m.v1.value, 0.5) self.assertFalse(m.v1.fixed) self.assertAlmostEqual(m.v2.value, 1.04816, delta=1e-5) self.assertAlmostEqual(m.v3.value, 1.34356, delta=1e-5) # Result obtained by solving the full system m_full = self._solve_ef_model_with_ipopt() self.assertAlmostEqual(m.v1.value, m_full.v1.value) self.assertAlmostEqual(m.v2.value, m_full.v2.value) self.assertAlmostEqual(m.v3.value, m_full.v3.value)
def set_input_values(self, input_values): solver = self._solver external_cons = self.external_cons external_vars = self.external_vars input_vars = self.input_vars for var, val in zip(input_vars, input_values): var.set_value(val) _temp = create_subsystem_block(external_cons, variables=external_vars) possible_input_vars = ComponentSet(input_vars) #for var in _temp.input_vars.values(): # # TODO: Is this check necessary? # assert var in possible_input_vars with TemporarySubsystemManager(to_fix=list(_temp.input_vars.values())): solver.solve(_temp) # Should we create the NLP from the original block or the temp block? # Need to create it from the original block because temp block won't # have residual constraints, whose derivatives are necessary. self._nlp = PyomoNLP(self._block)
def initialize_steady_without_solid_temperature(m): """ """ gas_inlet_names = set_gas_values_to_inlets(m) solid_inlet_names = set_solid_values_to_inlets(m) gas_phase = m.fs.MB.gas_phase solid_phase = m.fs.MB.solid_phase gas_length = m.fs.MB.gas_phase.length_domain solid_length = m.fs.MB.solid_phase.length_domain gas_disc_eqs = [ con for _, con, _ in generate_discretization_components_along_set( m, gas_length) ] solid_disc_eqs = [ con for _, con, _ in generate_discretization_components_along_set( m, solid_length) ] gas_sum_eqn_slice = gas_phase.properties[:, :].sum_component_eqn gas_sum_eqn_slice.attribute_errors_generate_exceptions = False solid_sum_eqn_slice = solid_phase.properties[:, :].sum_component_eqn solid_sum_eqn_slice.attribute_errors_generate_exceptions = False to_deactivate = [] to_deactivate.extend(gas_sum_eqn_slice) to_deactivate.extend(solid_sum_eqn_slice) to_deactivate.extend(gas_disc_eqs) to_deactivate.extend(solid_disc_eqs) to_fix = [] for name in gas_inlet_names + solid_inlet_names: ref = m.find_component(name) to_fix.extend(ref.values()) with TemporarySubsystemManager(to_fix=to_fix, to_deactivate=to_deactivate): solve_strongly_connected_components(m)
def set_input_values(self, input_values): solver = self._solver external_cons = self.external_cons external_vars = self.external_vars input_vars = self.input_vars for var, val in zip(input_vars, input_values): var.set_value(val) for block, inputs in self._scc_list: if len(block.vars) == 1: calculate_variable_from_constraint(block.vars[0], block.cons[0]) else: with TemporarySubsystemManager(to_fix=inputs): solver.solve(block) # Send updated variable values to NLP for dervative evaluation primals = self._nlp.get_primals() to_update = input_vars + external_vars indices = self._nlp.get_primal_indices(to_update) values = np.fromiter((var.value for var in to_update), float) primals[indices] = values self._nlp.set_primals(primals)
def main(): """ """ nxfe = 10 # Default = 10 # NOTE: Default inputs: (128.2, 591.4) ic_model_params = {"nxfe": nxfe} ic_inputs = { #"fs.MB.gas_phase.properties[*,0.0].flow_mol": 120.0, #"fs.MB.solid_phase.properties[*,1.0].flow_mass": 550.0, } 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 # NOTE: Decreasing size of model (horizon, tfe_width, and nxfe) # while developing NMPC workflow. Defaults: 900, 15, 10 n_nmpc_samples = 30 # For now. My target should probably be around 20-30 horizon = 1800 tfe_width = 60 sample_width = 180 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) ] nmpc_sample_points = [ float(sample_width*i) for i in range(n_nmpc_samples) # This is the "real time" at which NMPC inputs are applied # for each solve of the optimal control problem. ] 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"] = 900 # # 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) with open("ic_data.json", "w") as fp: json.dump(dae_data, fp) with open("setpoint_data.json", "w") as fp: json.dump(setpoint, fp) 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) #TODO: # Outside the loop: # - make plant model # - initialize data structure for plant data to initial condition # (one data structure for states, one for controls) # Inside the loop: # - initialize controller model (with bounds fixed) # - solve control problem # - extract first control input from controller model, # send to plant and data structure for inputs # - simulate plant # - extend plant state data structure with results of simulation # - update controller initial conditions with final value from plant # - update plant initial conditions with final value from plant plant_model_params = { "horizon": sample_width, "tfe_width": tfe_width, "ntcp": 1, "nxfe": nxfe, } m_plant = get_nmpc_plant_model( parameter_perturbation=disturbance, model_params=plant_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_plant) plant_time = m_plant.fs.time input_names = [ "fs.MB.gas_phase.properties[*,0.0].flow_mol", "fs.MB.solid_phase.properties[*,1.0].flow_mass", ] applied_inputs = ( [t0], {name: [m.find_component(name)[t0].value] for name in input_names}, ) # TODO: Initialize a planned_inputs sequence plant_scalar_vars, plant_dae_vars = flattened_vars plant_variables = list(plant_dae_vars) plant_variables.append(m_plant.tracking_cost) plant_data = initialize_time_series_data(plant_variables, plant_time, t0=t0) controller_dae_vars = [ m.find_component(var.referent) for var in plant_dae_vars ] controller_variables = list(controller_dae_vars) controller_variables.append(m.tracking_cost) # Assuming plant and controller have same initial conditions at this point for i in range(n_nmpc_samples): current_time = nmpc_sample_points[i] # # Initialze controller # 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] ) # Want an unbounded conversion for simulation. m.fs.MB.solid_phase.reactions[:,0.0].OC_conv.setlb(None) with TemporarySubsystemManager( to_fix=input_vardata, to_deactivate=[m.piecewise_constant_constraint], ): print("Initializing controller by time element...") with TIMER.context("elem-init-controller"): initialize_by_time_element(m, time, solver) m.fs.MB.solid_phase.reactions[:,0.0].OC_conv.setlb(0.89) ### # # Solve controller model # print("Starting dynamic optimization solve...") with TIMER.context("solve dynamic"): solver.solve(m, tee=True) ### # # Extract inputs from controller model # controller_inputs = ( list(sample_points), { name: [m.find_component(name)[ts].value for ts in sample_points] for name in input_names }, ) # We have two important input sequences to keep track of. One is the # past inputs, actually applied to the plant. The other is the planned # inputs. # TODO, here: # (a) Extract the first input from controller_inputs, use it to extend # the sequence of applied inputs # (b) Apply offset to this controller_inputs sequence, rename it # planned_inputs # Both of these have an offset applied # (c) Should happen first: extract first input from controller_inputs, # apply to plant # This is the extracted first input plant_inputs = ( [t0, sample_width], { name: [values[0], values[1]] for name, values in controller_inputs[1].items() }, ) # Add this extracted first input to the sequence of applied inputs applied_inputs[0].append(current_time + sample_width) for name, values in plant_inputs[1].items(): applied_inputs[1][name].append(values[1]) # Planned_inputs. These will be used in the case we cannot solve # a dynamic optimization problem. planned_inputs = ( [t + current_time for t in controller_inputs[0]], controller_inputs[1], ) # # Sent inputs into plant model # plant_inputs = interval_data_from_time_series(plant_inputs) load_inputs_into_model(m_plant, time, plant_inputs) ### # # Simulate plant model # print("Initializing plant by time element...") with TIMER.context("elem-init-plant"): initialize_by_time_element(m_plant, plant_time, solver) solver.solve(m_plant, tee=True) # Record data plant_data = extend_time_series_data( plant_data, plant_variables, plant_time, offset=current_time, ) ### pyo.Reference(m.fs.MB.solid_phase.properties[:, 0.0].temperature).pprint() # indiscriminately shift every time-indexed variable in the # model backwards one sample. Inputs, disturbances, everything... seen = set() for var in controller_dae_vars: if id(var[t0]) in seen: continue else: # We need to make sure we don't do this twice for the same # vardata. Note that we can encounter the same vardata multiple # times due to references. seen.add(id(var[t0])) for t in time: ts = t + sample_width idx = time.find_nearest_index(ts, tolerance=1e-8) if idx is None: # ts is outside the controller's horizon var[t].set_value(var[time.last()].value) else: ts = time.at(idx) var[t].set_value(var[ts].value) pyo.Reference(m.fs.MB.solid_phase.properties[:, 0.0].temperature).pprint() # # Re-initialize plant and controller to new initial conditions # tf = sample_width for i, var in enumerate(plant_dae_vars): final_value = var[tf].value for t in plant_time: var[t].set_value(final_value) controller_var = controller_dae_vars[i] controller_var[t0].set_value(final_value) ### plant_fname = "nmpc_plant_data.json" with open(plant_fname, "w") as fp: json.dump(plant_data, fp) # Note that this is not actually all the data I need. # I also need the setpoint data. input_fname = "nmpc_input_data.json" with open(input_fname, "w") as fp: json.dump(applied_inputs, fp)
def set_input_values(self, input_values): solver = self._solver external_cons = self.external_cons external_vars = self.external_vars input_vars = self.input_vars for var, val in zip(input_vars, input_values): var.set_value(val, skip_validation=True) vector_scc_idx = 0 for block, inputs in self._scc_list: if len(block.vars) == 1: calculate_variable_from_constraint(block.vars[0], block.cons[0]) else: if self._use_cyipopt: # Transfer variable values into the projected NLP, solve, # and extract values. nlp = self._vector_scc_nlps[vector_scc_idx] proj_nlp = self._vector_proj_nlps[vector_scc_idx] input_coords = self._vector_scc_input_coords[ vector_scc_idx] cyipopt = self._cyipopt_solvers[vector_scc_idx] _, local_inputs = self._vector_scc_list[vector_scc_idx] primals = nlp.get_primals() variables = nlp.get_pyomo_variables() # Set values and bounds from inputs to the SCC. # This works because values have been set in the original # pyomo model, either by a previous SCC solve, or from the # "global inputs" for i, var in zip(input_coords, local_inputs): # Set primals (inputs) in the original NLP primals[i] = var.value # This affects future evaluations in the ProjectedNLP nlp.set_primals(primals) x0 = proj_nlp.get_primals() sol, _ = cyipopt.solve(x0=x0) # Set primals from solution in projected NLP. This updates # values in the original NLP proj_nlp.set_primals(sol) # I really only need to set new primals for the variables in # the ProjectedNLP. However, I can only get a list of variables # from the original Pyomo NLP, so here some of the values I'm # setting are redundant. new_primals = nlp.get_primals() assert len(new_primals) == len(variables) for var, val in zip(variables, new_primals): var.set_value(val, skip_validation=True) else: # Use a Pyomo solver to solve this strongly connected # component. with TemporarySubsystemManager(to_fix=inputs): solver.solve(block) vector_scc_idx += 1 # Send updated variable values to NLP for dervative evaluation primals = self._nlp.get_primals() to_update = input_vars + external_vars indices = self._nlp.get_primal_indices(to_update) values = np.fromiter((var.value for var in to_update), float) primals[indices] = values self._nlp.set_primals(primals)
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()
def petsc_dae_by_time_element( m, time, timevar=None, initial_constraints=None, initial_variables=None, detect_initial=True, skip_initial=False, snes_options=None, ts_options=None, wsl=None, keepfiles=False, symbolic_solver_labels=True, vars_stub=None, trajectory_save_prefix=None, ): """Solve a DAE problem step by step using the PETSc DAE solver. This integrates from one time point to the next. Args: m (Block): Pyomo model to solve time (ContinuousSet): Time set timevar (Var): Optional specification of a time variable, which can be used to write constraints that are an explicit function of time. initial_constraints (list): Constraints to solve in the initial condition solve step. Since the time-indexed constraints are picked up automatically, this generally includes non-time-indexed constraints. initial_variables (list): This is a list of variables to fix after the initial condition solve step. If these variables were originally unfixed, they will be unfixed at the end of the solve. This usually includes non-time-indexed variables that are calculated along with the initial conditions. detect_initial (bool): If True, add non-time-indexed variables and constraints to initial_variables and initial_constraints. skip_initial (bool): Don't do the initial condition calculation step, and assume that the initial condition values have already been calculated. This can be useful, for example, if you read initial conditions from a separately solved steady state problem, or otherwise know the initial conditions. snes_options (dict): PETSc nonlinear equation solver options ts_options (dict): PETSc time-stepping solver options wsl (bool): if True use WSL to run PETSc, if False don't use WSL to run PETSc, if None automatic. The WSL is only for Windows. keepfiles (bool): pass to keepfiles arg for solvers symbolic_solver_labels (bool): pass to symbolic_solver_labels argument for solvers. If you want to read trajectory data from the time-stepping solver, this should be True. vars_stub (str or None): Copy the `*.col` and `*.typ` files to the working directory using this stub if not None. These are needed to interpret the trajectory data. trajectory_save_prefix (str or None): If a string is provided the trajectory data will be saved as gzipped json Returns: List of solver results objects from each solve. If there are initial condition constraints and they are not skipped, the first object will be from the initial condition solve. Then there should be one for each time element for each TS solve. """ solve_log = idaeslog.getSolveLogger("petsc-dae") regular_vars, time_vars = flatten_dae_components(m, time, pyo.Var) regular_cons, time_cons = flatten_dae_components(m, time, pyo.Constraint) tdisc = find_discretization_equations(m, time) solver_snes = pyo.SolverFactory("petsc_snes", options=snes_options, wsl=wsl) solver_dae = pyo.SolverFactory("petsc_ts", options=ts_options, wsl=wsl, vars_stub=vars_stub) if initial_variables is None: initial_variables = [] if initial_constraints is None: initial_constraints = [] if detect_initial: rvset = ComponentSet(regular_vars) rcset = ComponentSet(regular_cons) icset = ComponentSet(initial_constraints) ivset = ComponentSet(initial_variables) initial_variables = list(ivset | rvset) initial_constraints = list(icset | rcset) # First calculate the inital conditions and non-time-indexed constraints res_list = [] t0 = time.first() if not skip_initial: with TemporarySubsystemManager(to_deactivate=tdisc): constraints = [con[t0] for con in time_cons if t0 in con ] + initial_constraints variables = [var[t0] for var in time_vars] + initial_variables if len(constraints) > 0: # if the initial condition is specified and there are no # initial constraints, don't try to solve. t_block = create_subsystem_block( constraints, variables, ) # set up the scaling factor suffix _sub_problem_scaling_suffix(m, t_block) with idaeslog.solver_log(solve_log, idaeslog.INFO) as slc: res = solver_snes.solve(t_block, tee=slc.tee) res_list.append(res) tprev = t0 count = 1 fix_derivs = [] with TemporarySubsystemManager( to_deactivate=tdisc, to_fix=initial_variables + fix_derivs, ): # Solver time steps deriv_diff_map = _get_derivative_differential_data_map(m, time) for t in time: if t == time.first(): # t == time.first() was handled above continue constraints = [con[t] for con in time_cons if t in con] variables = [var[t] for var in time_vars] # Create a temporary block with references to original constraints # and variables so we can integrate this "subsystem" without # altering the rest of the model. t_block = create_subsystem_block(constraints, variables) differential_vars = _set_dae_suffixes_from_variables( t_block, variables, deriv_diff_map, ) # We need to check if there are derivatives in the problem before # sending this to the solver. We'll assume that if you are using # this and don't have any differential equations, you are making a # mistake. if len(differential_vars) < 1: raise RuntimeError( "No differential equations found at t = %s, " "you do not need a DAE solver." % t) if timevar is not None: t_block.dae_suffix[timevar[t]] = int(DaeVarTypes.TIME) # Set up the scaling factor suffix _sub_problem_scaling_suffix(m, t_block) # Take initial conditions for this step from the result of previous _copy_time(time_vars, tprev, t) with idaeslog.solver_log(solve_log, idaeslog.INFO) as slc: res = solver_dae.solve( t_block, tee=slc.tee, keepfiles=keepfiles, symbolic_solver_labels=symbolic_solver_labels, export_nonlinear_variables=differential_vars, options={ "--ts_init_time": tprev, "--ts_max_time": t }, ) if trajectory_save_prefix is not None: tj = PetscTrajectory(stub=vars_stub, delete_on_read=True, unscale=t_block) tj.to_json(f"{trajectory_save_prefix}_{count}.json.gz") tprev = t count += 1 res_list.append(res) return res_list
def solve_strongly_connected_components(block, solver=None, solve_kwds=None): """ This function solves a square block of variables and equality constraints by solving strongly connected components individually. Strongly connected components (of the directed graph of constraints obtained from a perfect matching of variables and constraints) are the diagonal blocks in a block triangularization of the incidence matrix, so solving the strongly connected components in topological order is sufficient to solve the entire block. Arguments --------- block: Pyomo Block The Pyomo block whose variables and constraints will be solved solver: Pyomo solver object The solver object that will be used to solve strongly connected components of size greater than one constraint. Must implement a solve method. solve_kwds: Dictionary Keyword arguments for the solver's solve method Returns ------- List of results objects returned by each call to solve """ if solve_kwds is None: solve_kwds = {} constraints = list(block.component_data_objects(Constraint, active=True)) var_set = ComponentSet() variables = [] for con in constraints: for var in identify_variables(con.expr, include_fixed=False): # Because we are solving, we do not want to include fixed variables if var not in var_set: variables.append(var) var_set.add(var) res_list = [] for scc, inputs in generate_strongly_connected_components( constraints, variables, ): with TemporarySubsystemManager(to_fix=inputs): if len(scc.vars) == 1: calculate_variable_from_constraint(scc.vars[0], scc.cons[0]) else: if solver is None: # NOTE: Use local name to avoid slow generation of this # error message if a user provides a large, non-decomposable # block with no solver. vars = [var.local_name for var in scc.vars.values()] cons = [con.local_name for con in scc.cons.values()] raise RuntimeError( "An external solver is required if block has strongly\n" "connected components of size greater than one (is not " "a DAG).\nGot an SCC with components: \n%s\n%s" % (vars, cons)) results = solver.solve(scc, **solve_kwds) res_list.append(results) return res_list