def test_set_dae_suffix(): m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() regular_vars, time_vars = pyodae.flatten.flatten_dae_components( m, m.t, pyo.Var) regular_cons, time_cons = pyodae.flatten.flatten_dae_components( m, m.t, pyo.Constraint) t = 180 m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) m.scaling_factor[m.ydot[180, 2]] = 10 constraints = [con[t] for con in time_cons if t in con] variables = [var[t] for var in time_vars] t_block = create_subsystem_block(constraints, variables) deriv_diff_map = petsc._get_derivative_differential_data_map(m, m.t) petsc._set_dae_suffixes_from_variables(t_block, variables, deriv_diff_map) petsc._sub_problem_scaling_suffix(m, t_block) assert t_block.dae_suffix[m.ydot[180, 1]] == 2 assert t_block.dae_suffix[m.ydot[180, 2]] == 2 assert t_block.dae_suffix[m.ydot[180, 3]] == 2 assert t_block.dae_suffix[m.ydot[180, 4]] == 2 assert t_block.dae_suffix[m.ydot[180, 5]] == 2 assert t_block.scaling_factor[m.ydot[180, 2]] == 10 assert t_block.dae_suffix[m.y[180, 1]] == 1 assert t_block.dae_suffix[m.y[180, 2]] == 1 assert t_block.dae_suffix[m.y[180, 3]] == 1 assert t_block.dae_suffix[m.y[180, 4]] == 1 assert t_block.dae_suffix[m.y[180, 5]] == 1 # Make sure deactivating a differential equation makes the variable that # would have been differential go algebraic m, y1, y2, y3, y4, y5, y6 = dae_with_non_time_indexed_constraint() # discretization equations would be deactivated in normal PETSc solve for con in petsc.find_discretization_equations(m, m.t): con.deactivate() # deactivate a differential equation making y4 be algebraic m.eq_ydot4[180].deactivate() regular_vars, time_vars = pyodae.flatten.flatten_dae_components( m, m.t, pyo.Var) regular_cons, time_cons = pyodae.flatten.flatten_dae_components( m, m.t, pyo.Constraint) t = 180 constraints = [con[t] for con in time_cons if t in con] variables = [var[t] for var in time_vars] t_block = create_subsystem_block(constraints, variables) deriv_diff_map = petsc._get_derivative_differential_data_map(m, m.t) petsc._set_dae_suffixes_from_variables(t_block, variables, deriv_diff_map) petsc._sub_problem_scaling_suffix(m, t_block) assert m.y[t, 4] not in t_block.dae_suffix assert m.ydot[t, 4] not in t_block.dae_suffix
def test_subsystem_inputs_only(self): m = _make_simple_model() cons = [m.con2, m.con3] block = create_subsystem_block(cons) self.assertEqual(len(block.vars), 0) self.assertEqual(len(block.input_vars), 4) self.assertEqual(len(block.cons), 2) self.assertEqual(len([v for v in block.component_data_objects(pyo.Var) if not v.fixed]), 4) block.input_vars.fix() self.assertEqual(len([v for v in block.component_data_objects(pyo.Var) if not v.fixed]), 0) var_set = ComponentSet([m.v1, m.v2, m.v3, m.v4]) self.assertIs(block.cons[0], m.con2) self.assertIs(block.cons[1], m.con3) self.assertIn(block.input_vars[0], var_set) self.assertIn(block.input_vars[1], var_set) self.assertIn(block.input_vars[2], var_set) self.assertIn(block.input_vars[3], var_set) # Make sure block is not part of the original model's tree. We # don't want to alter the user's model at all. self.assertIsNot(block.model(), m) # Components on the block are references to components on the # original model for comp in block.component_objects((pyo.Var, pyo.Constraint)): self.assertTrue(comp.is_reference()) for data in comp.values(): self.assertIs(data.model(), m)
def test_solve_subsystem(self): # This is a test of this function's intended use. We extract a # subsystem then solve it without altering the rest of the model. m = _make_simple_model() ipopt = pyo.SolverFactory("ipopt") m.v5 = pyo.Var(initialize=1.0) m.c4 = pyo.Constraint(expr=m.v5 == 5.0) cons = [m.con2, m.con3] vars = [m.v1, m.v2] block = create_subsystem_block(cons, vars) m.v3.fix(1.0) m.v4.fix(2.0) # Initialize to avoid converging infeasible due to bad pivots m.v1.set_value(1.0) m.v2.set_value(1.0) ipopt.solve(block) # 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) # Rest of model has not changed self.assertEqual(m.v5.value, 1.0)
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 get_hessian_of_constraint(constraint, wrt1=None, wrt2=None, nlp=None): constraints = [constraint] if wrt1 is None and wrt2 is None: variables = list( identify_variables(constraint.expr, include_fixed=False)) wrt1 = variables wrt2 = variables elif wrt1 is not None and wrt2 is not None: variables = wrt1 + wrt2 elif wrt1 is not None: # but wrt2 is None wrt2 = wrt1 variables = wrt1 else: # wrt2 is not None and wrt1 is None wrt1 = wrt2 variables = wrt1 if nlp is None: block = create_subsystem_block(constraints, variables=variables) # Could fix input_vars so I don't evaluate the Hessian with respect # to variables I don't care about... # HUGE HACK: Variables not included in a constraint are not written # to the nl file, so we cannot take the derivative with respect to # them, even though we know this derivative is zero. To work around, # we make sure all variables appear on the block in the form of a # dummy constraint. Then we can take derivatives of any constraint # with respect to them. Conveniently, the extract_submatrix_ # call deals with extracting the variables and constraint we care # about, in the proper order. block._dummy_var = Var() block._dummy_con = Constraint(expr=sum(variables) == block._dummy_var) block._obj = Objective(expr=0.0) nlp = PyomoNLP(block) saved_duals = nlp.get_duals() saved_obj_factor = nlp.get_obj_factor() temp_duals = np.zeros(len(saved_duals)) # NOTE: This makes some assumption about how the Lagrangian is constructed. # TODO: Define the convention we assume and convert if necessary. idx = nlp.get_constraint_indices(constraints)[0] temp_duals[idx] = 1.0 nlp.set_duals(temp_duals) nlp.set_obj_factor(0.0) # NOTE: The returned matrix preserves explicit zeros. I.e. it contains # coordinates for every entry that could possibly be nonzero. submatrix = nlp.extract_submatrix_hessian_lag(wrt1, wrt2) nlp.set_obj_factor(saved_obj_factor) nlp.set_duals(saved_duals) return submatrix
def get_numeric_incidence_matrix(variables, constraints): """ This function gets the numeric incidence matrix (Jacobian) of Pyomo constraints with respect to variables. """ # NOTE: There are several ways to get a numeric incidence matrix # from a Pyomo model. Here we get the numeric incidence matrix by # creating a temporary block and using the PyNumero ASL interface. comps = list(variables) + list(constraints) _check_unindexed(comps) block = create_subsystem_block(constraints, variables) block._obj = Objective(expr=0) nlp = PyomoNLP(block) return nlp.extract_submatrix_jacobian(variables, constraints)
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 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 __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