def test_dynamic_backward_disc_without_initial_conditions(self): nfe = 5 m = make_dynamic_model(nfe=nfe, scheme="BACKWARD") time = m.time t0 = m.time.first() t1 = m.time.next(t0) m.flow_in.fix() m.height[t0].fix() m.flow_out[t0].fix() m.dhdt[t0].fix() m.diff_eqn[t0].deactivate() m.flow_out_eqn[t0].deactivate() constraints = list( m.component_data_objects(pyo.Constraint, active=True)) self.assertEqual( len(list(generate_strongly_connected_components(constraints))), nfe, ) for i, (block, inputs) in enumerate( generate_strongly_connected_components(constraints)): with TemporarySubsystemManager(to_fix=inputs): # We have a much easier time testing the SCCs generated # in this test. t = m.time[i + 2] t_prev = m.time.prev(t) con_set = ComponentSet( [m.diff_eqn[t], m.flow_out_eqn[t], m.dhdt_disc_eq[t]]) var_set = ComponentSet([m.height[t], m.dhdt[t], m.flow_out[t]]) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(var_set), len(block.vars)) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) self.assertFalse(var.fixed) other_var_set = ComponentSet([m.height[t_prev]])\ if t != t1 else ComponentSet() # At t1, "input var" height[t0] is fixed, so # it is not included here. self.assertEqual(len(inputs), len(other_var_set)) for var in block.input_vars[:]: self.assertIn(var, other_var_set) self.assertTrue(var.fixed) for t in time: if t == t0: self.assertTrue(m.height[t].fixed) self.assertTrue(m.flow_out[t].fixed) self.assertTrue(m.dhdt[t].fixed) else: self.assertFalse(m.height[t].fixed) self.assertFalse(m.flow_out[t].fixed) self.assertFalse(m.dhdt[t].fixed)
def __init__( self, input_vars, external_vars, residual_cons, external_cons, solver=None, ): if solver is None: solver = SolverFactory("ipopt") self._solver = solver # We only need this block to construct the NLP, which wouldn't # be necessary if we could compute Hessians of Pyomo constraints. self._block = create_subsystem_block( residual_cons + external_cons, input_vars + external_vars, ) self._block._obj = Objective(expr=0.0) self._nlp = PyomoNLP(self._block) self._scc_list = list( generate_strongly_connected_components(external_cons, variables=external_vars)) assert len(external_vars) == len(external_cons) self.input_vars = input_vars self.external_vars = external_vars self.residual_cons = residual_cons self.external_cons = external_cons self.residual_con_multipliers = [None for _ in residual_cons] self.residual_scaling_factors = None
def test_dynamic_forward_disc(self): nfe = 5 m = make_dynamic_model(nfe=nfe, scheme="FORWARD") time = m.time t0 = m.time.first() t1 = m.time.next(t0) m.flow_in.fix() m.height[t0].fix() constraints = list(m.component_data_objects(pyo.Constraint)) # For a forward discretization, the entire model decomposes self.assertEqual( len(list(generate_strongly_connected_components(constraints))), len(list(m.component_data_objects(pyo.Constraint))), ) self.assertEqual( len(list(generate_strongly_connected_components(constraints))), 3 * nfe + 2, # "Initial constraints" only add two variables/equations ) for i, (block, inputs) in enumerate( generate_strongly_connected_components(constraints)): with TemporarySubsystemManager(to_fix=inputs): # The order is: # algebraic -> derivative -> differential -> algebraic -> ... idx = i // 3 mod = i % 3 t = m.time[idx + 1] if t != time.last(): t_next = m.time.next(t) self.assertEqual(len(block.vars), 1) self.assertEqual(len(block.cons), 1) if mod == 0: self.assertIs(block.vars[0], m.flow_out[t]) self.assertIs(block.cons[0], m.flow_out_eqn[t]) elif mod == 1: self.assertIs(block.vars[0], m.dhdt[t]) self.assertIs(block.cons[0], m.diff_eqn[t]) elif mod == 2: # Never get to mod == 2 when t == time.last() self.assertIs(block.vars[0], m.height[t_next]) self.assertIs(block.cons[0], m.dhdt_disc_eq[t])
def test_dynamic_backward_with_inputs(self): nfe = 5 m = make_dynamic_model(nfe=nfe, scheme="BACKWARD") time = m.time t0 = m.time.first() t1 = m.time.next(t0) # Initial conditions are still fixed m.height[t0].fix() m.flow_out[t0].fix() m.dhdt[t0].fix() m.diff_eqn[t0].deactivate() m.flow_out_eqn[t0].deactivate() # Variables that we want in our SCCs: # Here we exclude "dynamic inputs" (flow_in) instead of fixing them variables = [ var for var in m.component_data_objects(pyo.Var) if not var.fixed and var.parent_component() is not m.flow_in ] constraints = list( m.component_data_objects(pyo.Constraint, active=True)) self.assertEqual( len( list( generate_strongly_connected_components( constraints, variables, ))), nfe, ) # The result of the generator is the same as in the previous # test, but we are using the more general API for i, (block, inputs) in enumerate( generate_strongly_connected_components( constraints, variables, )): with TemporarySubsystemManager(to_fix=inputs): t = m.time[i + 2] t_prev = m.time.prev(t) con_set = ComponentSet( [m.diff_eqn[t], m.flow_out_eqn[t], m.dhdt_disc_eq[t]]) var_set = ComponentSet([m.height[t], m.dhdt[t], m.flow_out[t]]) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(var_set), len(block.vars)) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) self.assertFalse(var.fixed) other_var_set = ComponentSet([m.flow_in[t]]) if t != t1: other_var_set.add(m.height[t_prev]) # At t1, "input var" height[t0] is fixed, so # it is not included here. self.assertEqual(len(inputs), len(other_var_set)) for var in block.input_vars[:]: self.assertIn(var, other_var_set) self.assertTrue(var.fixed) for t in time: if t == t0: self.assertTrue(m.height[t].fixed) self.assertTrue(m.flow_out[t].fixed) self.assertTrue(m.dhdt[t].fixed) else: self.assertFalse(m.height[t].fixed) self.assertFalse(m.flow_out[t].fixed) self.assertFalse(m.dhdt[t].fixed)
def test_dynamic_backward_disc_with_initial_conditions(self): nfe = 5 m = make_dynamic_model(nfe=nfe, scheme="BACKWARD") time = m.time t0 = m.time.first() t1 = m.time.next(t0) m.flow_in.fix() m.height[t0].fix() constraints = list(m.component_data_objects(pyo.Constraint)) self.assertEqual( len(list(generate_strongly_connected_components(constraints))), nfe + 2, # The "initial constraints" have two SCCs because they # decompose into the algebraic equation and differential # equation. This decomposition is because the discretization # equation is not present. # # This is actually quite troublesome for testing because # it means that the topological order of strongly connected # components is not unique (alternatively, the initial # conditions and rest of the model are independent, or the # bipartite graph of variables and equations is disconnected). ) t_scc_map = {} for i, (block, inputs) in enumerate( generate_strongly_connected_components(constraints)): with TemporarySubsystemManager(to_fix=inputs): t = block.vars[0].index() t_scc_map[t] = i if t == t0: continue else: t_prev = m.time.prev(t) con_set = ComponentSet( [m.diff_eqn[t], m.flow_out_eqn[t], m.dhdt_disc_eq[t]]) var_set = ComponentSet( [m.height[t], m.dhdt[t], m.flow_out[t]]) self.assertEqual(len(con_set), len(block.cons)) self.assertEqual(len(var_set), len(block.vars)) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) self.assertFalse(var.fixed) other_var_set = ComponentSet([m.height[t_prev]])\ if t != t1 else ComponentSet() # At t1, "input var" height[t0] is fixed, so # it is not included here. self.assertEqual(len(inputs), len(other_var_set)) for var in block.input_vars[:]: self.assertIn(var, other_var_set) self.assertTrue(var.fixed) scc = -1 for t in m.time: if t == t0: self.assertTrue(m.height[t].fixed) else: self.assertFalse(m.height[t].fixed) # Make sure "finite element blocks" in the SCC DAG are # in a valid topological order self.assertGreater(t_scc_map[t], scc) scc = t_scc_map[t] self.assertFalse(m.flow_out[t].fixed) self.assertFalse(m.dhdt[t].fixed) self.assertTrue(m.flow_in[t].fixed)
def test_gas_expansion(self): N = 5 m = make_gas_expansion_model(N) m.rho[0].fix() m.F[0].fix() m.T[0].fix() constraints = list(m.component_data_objects(pyo.Constraint)) self.assertEqual( len(list(generate_strongly_connected_components(constraints))), N + 1, ) for i, (block, inputs) in enumerate( generate_strongly_connected_components(constraints)): with TemporarySubsystemManager(to_fix=inputs): if i == 0: # P[0], ideal_gas[0] self.assertEqual(len(block.vars), 1) self.assertEqual(len(block.cons), 1) var_set = ComponentSet([m.P[i]]) con_set = ComponentSet([m.ideal_gas[i]]) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) # Other variables are fixed; not included self.assertEqual(len(block.input_vars), 0) elif i == 1: # P[1], rho[1], F[1], T[1], etc. self.assertEqual(len(block.vars), 4) self.assertEqual(len(block.cons), 4) var_set = ComponentSet([m.P[i], m.rho[i], m.F[i], m.T[i]]) con_set = ComponentSet( [m.ideal_gas[i], m.mbal[i], m.ebal[i], m.expansion[i]]) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) # P[0] is in expansion[1] other_var_set = ComponentSet([m.P[i - 1]]) self.assertEqual(len(block.input_vars), 1) for var in block.input_vars[:]: self.assertIn(var, other_var_set) else: # P[i], rho[i], F[i], T[i], etc. self.assertEqual(len(block.vars), 4) self.assertEqual(len(block.cons), 4) var_set = ComponentSet([m.P[i], m.rho[i], m.F[i], m.T[i]]) con_set = ComponentSet( [m.ideal_gas[i], m.mbal[i], m.ebal[i], m.expansion[i]]) for var, con in zip(block.vars[:], block.cons[:]): self.assertIn(var, var_set) self.assertIn(con, con_set) # P[i-1], rho[i-1], F[i-1], T[i-1], etc. other_var_set = ComponentSet( [m.P[i - 1], m.rho[i - 1], m.F[i - 1], m.T[i - 1]]) self.assertEqual(len(block.input_vars), 4) for var in block.input_vars[:]: self.assertIn(var, other_var_set)
def __init__( self, input_vars, external_vars, residual_cons, external_cons, use_cyipopt=None, solver=None, ): """ Arguments: ---------- input_vars: list List of variables sent to this system by the outer solver external_vars: list List of variables that are solved for internally by this system residual_cons: list List of equality constraints whose residuals are exposed to the outer solver external_cons: list List of equality constraints used to solve for the external variables use_cyipopt: bool Whether to use CyIpopt to solve strongly connected components of the implicit function that have dimension greater than one. solver: Pyomo solver object Used to solve strongly connected components of the implicit function that have dimension greater than one. Only used if use_cyipopt is False. """ if use_cyipopt is None: use_cyipopt = cyipopt_available if use_cyipopt and not cyipopt_available: raise RuntimeError( "Constructing an ExternalPyomoModel with CyIpopt unavailable. " "Please set the use_cyipopt argument to False.") if solver is not None and use_cyipopt: raise RuntimeError( "Constructing an ExternalPyomoModel with a solver specified " "and use_cyipopt set to True. Please set use_cyipopt to False " "to use the desired solver.") elif solver is None and not use_cyipopt: solver = SolverFactory("ipopt") # If use_cyipopt is True, this solver is None and will not be used. self._solver = solver self._use_cyipopt = use_cyipopt # We only need this block to construct the NLP, which wouldn't # be necessary if we could compute Hessians of Pyomo constraints. self._block = create_subsystem_block( residual_cons + external_cons, input_vars + external_vars, ) self._block._obj = Objective(expr=0.0) self._nlp = PyomoNLP(self._block) self._scc_list = list( generate_strongly_connected_components(external_cons, variables=external_vars)) if use_cyipopt: # Using CyIpopt allows us to solve inner problems without # costly rewriting of the nl file. It requires quite a bit # of preprocessing, however, to construct the ProjectedNLP # for each block of the decomposition. # Get "vector-valued" SCCs, those of dimension > 0. # We will solve these with a direct IPOPT interface, which requires # some preprocessing. self._vector_scc_list = [(scc, inputs) for scc, inputs in self._scc_list if len(scc.vars) > 1] # Need a dummy objective to create an NLP for scc, inputs in self._vector_scc_list: scc._obj = Objective(expr=0.0) # I need scaling_factor so Pyomo NLPs I create from these blocks # don't break when ProjectedNLP calls get_primals_scaling scc.scaling_factor = Suffix(direction=Suffix.EXPORT) # HACK: scaling_factor just needs to be nonempty. scc.scaling_factor[scc._obj] = 1.0 # These are the "original NLPs" that will be projected self._vector_scc_nlps = [ PyomoNLP(scc) for scc, inputs in self._vector_scc_list ] self._vector_scc_var_names = [[ var.name for var in scc.vars.values() ] for scc, inputs in self._vector_scc_list] self._vector_proj_nlps = [ ProjectedNLP(nlp, names) for nlp, names in zip( self._vector_scc_nlps, self._vector_scc_var_names) ] # We will solve the ProjectedNLPs rather than the original NLPs self._cyipopt_nlps = [ CyIpoptNLP(nlp) for nlp in self._vector_proj_nlps ] self._cyipopt_solvers = [ CyIpoptSolver(nlp) for nlp in self._cyipopt_nlps ] self._vector_scc_input_coords = [ nlp.get_primal_indices(inputs) for nlp, (scc, inputs) in zip( self._vector_scc_nlps, self._vector_scc_list) ] assert len(external_vars) == len(external_cons) self.input_vars = input_vars self.external_vars = external_vars self.residual_cons = residual_cons self.external_cons = external_cons self.residual_con_multipliers = [None for _ in residual_cons] self.residual_scaling_factors = None