def test_identify_vars_expr(self): # # Identify variables when there are duplicates # m = ConcreteModel() m.a = Var(initialize=1) m.b = Var(initialize=2) m.e = Expression(expr=3*m.a) m.E = Expression([0,1], initialize={0:3*m.a, 1:4*m.b}) self.assertEqual( list(identify_variables(m.b+m.e)), [ m.b, m.a ] ) self.assertEqual( list(identify_variables(m.E[0])), [ m.a ] ) self.assertEqual( list(identify_variables(m.E[1])), [ m.b ] )
def test_identify_variables(self): M = ConcreteModel() M.x = Var() M.y = Var() M.w = Var() M.w = 2 M.w.fixed = True e = sin(M.x) + M.x*M.w + 3 v = list(str(v) for v in identify_variables(e)) self.assertEqual(v, ['x', 'w']) v = list(str(v) for v in identify_variables(e, include_fixed=False)) self.assertEqual(v, ['x'])
def __init__(self, comp): self._vars = ComponentSet() self._saved_bounds = list() if comp.type() == Constraint: if comp.is_indexed(): for c in comp.values(): self._vars.update(identify_variables(c.body)) else: self._vars.update(identify_variables(comp.body)) else: for c in comp.component_data_objects(Constraint, descend_into=True, active=True, sort=True): self._vars.update(identify_variables(c.body))
def add_affine_cuts(nlp_result, solve_data, config): with time_code(solve_data.timing, "affine cut generation"): m = solve_data.linear_GDP if config.calc_disjunctive_bounds: with time_code(solve_data.timing, "disjunctive variable bounding"): TransformationFactory('contrib.compute_disj_var_bounds').apply_to( m, solver=config.mip_solver if config.obbt_disjunctive_bounds else None ) config.logger.info("Adding affine cuts.") GDPopt = m.GDPopt_utils counter = 0 for var, val in zip(GDPopt.variable_list, nlp_result.var_values): if val is not None and not var.fixed: var.value = val for constr in constraints_in_True_disjuncts(m, config): # Note: this includes constraints that are deactivated in the current model (linear_GDP) disjunctive_var_bounds = disjunctive_bounds(constr.parent_block()) if constr.body.polynomial_degree() in (1, 0): continue vars_in_constr = list( identify_variables(constr.body)) if any(var.value is None for var in vars_in_constr): continue # a variable has no values # mcpp stuff mc_eqn = mc(constr.body, disjunctive_var_bounds) # mc_eqn = mc(constr.body) ccSlope = mc_eqn.subcc() cvSlope = mc_eqn.subcv() ccStart = mc_eqn.concave() cvStart = mc_eqn.convex() ub_int = min(constr.upper, mc_eqn.upper()) if constr.has_ub() else mc_eqn.upper() lb_int = max(constr.lower, mc_eqn.lower()) if constr.has_lb() else mc_eqn.lower() parent_block = constr.parent_block() # Create a block on which to put outer approximation cuts. aff_utils = parent_block.component('GDPopt_aff') if aff_utils is None: aff_utils = parent_block.GDPopt_aff = Block( doc="Block holding affine constraints") aff_utils.GDPopt_aff_cons = ConstraintList() aff_cuts = aff_utils.GDPopt_aff_cons concave_cut = sum(ccSlope[var] * (var - var.value) for var in vars_in_constr ) + ccStart >= lb_int convex_cut = sum(cvSlope[var] * (var - var.value) for var in vars_in_constr ) + cvStart <= ub_int aff_cuts.add(expr=concave_cut) aff_cuts.add(expr=convex_cut) counter += 2 config.logger.info("Added %s affine cuts" % counter)
def test_identify_duplicate_vars(self): # # Identify variables when there are duplicates # m = ConcreteModel() m.a = Var(initialize=1) #self.assertEqual( list(identify_variables(2*m.a+2*m.a, allow_duplicates=True)), # [ m.a, m.a ] ) self.assertEqual( list(identify_variables(2*m.a+2*m.a)), [ m.a ] )
def test_identify_vars_params(self): m = ConcreteModel() m.I = RangeSet(3) m.a = Param(initialize=1) m.b = Param(m.I, initialize=1, mutable=True) # # There are no variables in expressions with only parameters # self.assertEqual( list(identify_variables(m.a)), [] ) self.assertEqual( list(identify_variables(m.b[1])), [] ) self.assertEqual( list(identify_variables(m.a+m.b[1])), [] ) self.assertEqual( list(identify_variables(m.a**m.b[1])), [] ) self.assertEqual( list(identify_variables( m.a**m.b[1] + m.b[2])), [] ) self.assertEqual( list(identify_variables( m.a**m.b[1] + m.b[2]*m.b[3]*m.b[2])), [] )
def __init__(self, expression, improved_var_bounds=None): super(MCPP_visitor, self).__init__() self.mcpp = _MCPP_lib() self.missing_value_warnings = [] self.expr = expression vars = list(identify_variables(expression, include_fixed=False)) self.num_vars = len(vars) # Map expression variables to MC variables self.known_vars = ComponentMap() # Map expression variables to their index self.var_to_idx = ComponentMap() # Pre-register all variables inf = float('inf') for i, var in enumerate(vars): self.var_to_idx[var] = i # check if improved variable bound is provided if improved_var_bounds is not None: lb, ub = improved_var_bounds.get(var, (-inf, inf)) else: lb, ub = -inf, inf self.known_vars[var] = self.register_var(var, lb, ub) self.refs = None
def test_identify_vars_numeric(self): # # There are no variables in a constant expression # self.assertEqual(list(identify_variables(5)), [])
def test_identify_vars_vars(self): m = ConcreteModel() m.I = RangeSet(3) m.a = Var(initialize=1) m.b = Var(m.I, initialize=1) m.p = Param(initialize=1, mutable=True) m.x = ExternalFunction(library='foo.so', function='bar') # # Identify variables in various algebraic expressions # self.assertEqual(list(identify_variables(m.a)), [m.a]) self.assertEqual(list(identify_variables(m.b[1])), [m.b[1]]) self.assertEqual(list(identify_variables(m.a + m.b[1])), [m.a, m.b[1]]) self.assertEqual(list(identify_variables(m.a**m.b[1])), [m.a, m.b[1]]) self.assertEqual(list(identify_variables(m.a**m.b[1] + m.b[2])), [m.a, m.b[1], m.b[2]]) self.assertEqual( list(identify_variables(m.a**m.b[1] + m.b[2] * m.b[3] * m.b[2])), [m.a, m.b[1], m.b[2], m.b[3]]) self.assertEqual( list(identify_variables(m.a**m.b[1] + m.b[2] / m.b[3] * m.b[2])), [m.a, m.b[1], m.b[2], m.b[3]]) # # Identify variables in the arguments to functions # self.assertEqual( list(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), [m.a, m.b[1]]) self.assertEqual( list(identify_variables(m.x(m.p, 'string_param', 1, []) * m.b[1])), [m.b[1]]) self.assertEqual(list(identify_variables(tanh(m.a) * m.b[1])), [m.a, m.b[1]]) self.assertEqual(list(identify_variables(abs(m.a) * m.b[1])), [m.a, m.b[1]]) # # Check logic for allowing duplicates # self.assertEqual(list(identify_variables(m.a**m.a + m.a)), [m.a])
def test_identify_vars_linear_expression(self): m = ConcreteModel() m.x = Var() expr = quicksum([m.x, m.x], linear=True) self.assertEqual(list(identify_variables(expr, include_fixed=False)), [m.x])
def solve_strongly_connected_components( block, solver=None, solve_kwds=None, calc_var_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. One-by-one blocks are solved using Pyomo's calculate_variable_from_constraint function, while higher-dimension blocks are solved using the user-provided solver object. 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 calc_var_kwds: Dictionary Keyword arguments for calculate_variable_from_constraint Returns ------- List of results objects returned by each call to solve """ if solve_kwds is None: solve_kwds = {} if calc_var_kwds is None: calc_var_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: results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds) res_list.append(results) 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
def coefficient_matching(model, constraint, uncertain_params, config): ''' :param model: master problem model :param constraint: the constraint from the master problem model :param uncertain_params: the list of uncertain parameters :param first_stage_variables: the list of effective first-stage variables (includes ssv if decision_rule_order = 0) :return: True if the coefficient matching was successful, False if its proven robust_infeasible due to constraints of the form 1 == 0 ''' # === Returned flags successful_matching = True robust_infeasible = False # === Efficiency for q_LB = q_UB actual_uncertain_params = [] for i in range(len(uncertain_params)): if not is_certain_parameter(uncertain_param_index=i, config=config): actual_uncertain_params.append(uncertain_params[i]) # === Add coefficient matching constraint list if not hasattr(model, "coefficient_matching_constraints"): model.coefficient_matching_constraints = ConstraintList() if not hasattr(model, "swapped_constraints"): model.swapped_constraints = ConstraintList() variables_in_constraint = ComponentSet(identify_variables(constraint.expr)) params_in_constraint = ComponentSet(identify_mutable_parameters(constraint.expr)) first_stage_variables = model.util.first_stage_variables second_stage_variables = model.util.second_stage_variables # === Determine if we need to do DR expression/ssv substitution to # make h(x,z,q) == 0 into h(x,d,q) == 0 (which is just h(x,q) == 0) if all(v in ComponentSet(first_stage_variables) for v in variables_in_constraint) and \ any(q in ComponentSet(actual_uncertain_params) for q in params_in_constraint): # h(x, q) == 0 pass elif all(v in ComponentSet(first_stage_variables + second_stage_variables) for v in variables_in_constraint) and \ any(q in ComponentSet(actual_uncertain_params) for q in params_in_constraint): constraint = substitute_ssv_in_dr_constraints(model=model, constraint=constraint) variables_in_constraint = ComponentSet(identify_variables(constraint.expr)) params_in_constraint = ComponentSet(identify_mutable_parameters(constraint.expr)) else: pass if all(v in ComponentSet(first_stage_variables) for v in variables_in_constraint) and \ any(q in ComponentSet(actual_uncertain_params) for q in params_in_constraint): # Swap param objects for variable objects in this constraint model.param_set = [] for i in range(len(list(variables_in_constraint))): # Initialize Params to non-zero value due to standard_repn bug model.add_component("p_%s" % i, Param(initialize=1, mutable=True)) model.param_set.append(getattr(model, "p_%s" % i)) model.variable_set = [] for i in range(len(list(actual_uncertain_params))): model.add_component("x_%s" % i, Var(initialize=1)) model.variable_set.append(getattr(model, "x_%s" % i)) original_var_to_param_map = list(zip(list(variables_in_constraint), model.param_set)) original_param_to_vap_map = list(zip(list(actual_uncertain_params), model.variable_set)) var_to_param_substitution_map_forward = {} # Separation problem initialized to nominal uncertain parameter values for var, param in original_var_to_param_map: var_to_param_substitution_map_forward[id(var)] = param param_to_var_substitution_map_forward = {} # Separation problem initialized to nominal uncertain parameter values for param, var in original_param_to_vap_map: param_to_var_substitution_map_forward[id(param)] = var var_to_param_substitution_map_reverse = {} # Separation problem initialized to nominal uncertain parameter values for var, param in original_var_to_param_map: var_to_param_substitution_map_reverse[id(param)] = var param_to_var_substitution_map_reverse = {} # Separation problem initialized to nominal uncertain parameter values for param, var in original_param_to_vap_map: param_to_var_substitution_map_reverse[id(var)] = param model.swapped_constraints.add( replace_expressions( expr=replace_expressions(expr=constraint.lower, substitution_map=param_to_var_substitution_map_forward), substitution_map=var_to_param_substitution_map_forward) == replace_expressions( expr=replace_expressions(expr=constraint.body, substitution_map=param_to_var_substitution_map_forward), substitution_map=var_to_param_substitution_map_forward)) swapped = model.swapped_constraints[max(model.swapped_constraints.keys())] val = generate_standard_repn(swapped.body, compute_values=False) if val.constant is not None: if type(val.constant) not in native_types: temp_expr = replace_expressions(val.constant, substitution_map=var_to_param_substitution_map_reverse) if temp_expr.is_potentially_variable(): model.coefficient_matching_constraints.add(expr=temp_expr == 0) elif math.isclose(value(temp_expr), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True elif math.isclose(value(val.constant), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True if val.linear_coefs is not None: for coeff in val.linear_coefs: if type(coeff) not in native_types: temp_expr = replace_expressions(coeff, substitution_map=var_to_param_substitution_map_reverse) if temp_expr.is_potentially_variable(): model.coefficient_matching_constraints.add(expr=temp_expr == 0) elif math.isclose(value(temp_expr), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True elif math.isclose(value(coeff), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True if val.quadratic_coefs: for coeff in val.quadratic_coefs: if type(coeff) not in native_types: temp_expr = replace_expressions(coeff, substitution_map=var_to_param_substitution_map_reverse) if temp_expr.is_potentially_variable(): model.coefficient_matching_constraints.add(expr=temp_expr == 0) elif math.isclose(value(temp_expr), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True elif math.isclose(value(coeff), 0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL): pass else: successful_matching = False robust_infeasible = True if val.nonlinear_expr is not None: successful_matching = False robust_infeasible = False if successful_matching: model.util.h_x_q_constraints.add(constraint) for i in range(len(list(variables_in_constraint))): model.del_component("p_%s" % i) for i in range(len(list(params_in_constraint))): model.del_component("x_%s" % i) model.del_component("swapped_constraints") model.del_component("swapped_constraints_index") return successful_matching, robust_infeasible
def test_identify_vars_numeric(self): # # There are no variables in a constant expression # self.assertEqual( list(identify_variables(5)), [] )
def generate_strongly_connected_components( constraints, variables=None, include_fixed=False, ): """ Performs a block triangularization of the incidence matrix of the provided constraints and variables, and yields a block that contains the constraints and variables of each diagonal block (strongly connected component). Arguments --------- constraints: List of Pyomo constraint data objects Constraints used to generate strongly connected components. variables: List of Pyomo variable data objects Variables that may participate in strongly connected components. If not provided, all variables in the constraints will be used. include_fixed: Bool Indicates whether fixed variables will be included when identifying variables in constraints. Yields ------ Blocks containing the variables and constraints of every strongly connected component, in a topological order, as well as the "input variables" for that block """ if variables is None: var_set = ComponentSet() variables = [] for con in constraints: for var in identify_variables( con.expr, include_fixed=include_fixed, ): if var not in var_set: variables.append(var) var_set.add(var) assert len(variables) == len(constraints) igraph = IncidenceGraphInterface() var_block_map, con_block_map = igraph.block_triangularize( variables=variables, constraints=constraints, ) blocks = set(var_block_map.values()) n_blocks = len(blocks) var_blocks = [[] for b in range(n_blocks)] con_blocks = [[] for b in range(n_blocks)] for var, b in var_block_map.items(): var_blocks[b].append(var) for con, b in con_block_map.items(): con_blocks[b].append(con) subsets = list(zip(con_blocks, var_blocks)) for block, inputs in generate_subsystem_blocks( subsets, include_fixed=include_fixed, ): # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs)
def test_init_indexed(self): block_set = pyo.Set(initialize=[0, 1, 2]) block_set.construct() horizon_map = {0: 1., 1: 3., 2: 5.} nfe_map = {0: 2, 1: 6, 2: 10} model_map = { i: make_model(horizon_map[i], nfe_map[i]) for i in block_set } time_map = {i: model_map[i].time for i in block_set} inputs_map = {i: [model_map[i].flow_in[0]] for i in block_set} measurements_map = { i: [model_map[i].conc[0, 'A'], model_map[i].conc[0, 'B']] for i in block_set } # Construct block with a dict for each of its arguments block = DynamicBlock( block_set, model=model_map, time=time_map, inputs=inputs_map, measurements=measurements_map, ) # Make sure we have the right type assert type(block) is IndexedDynamicBlock assert isinstance(block, DynamicBlock) block.construct() assert all(b.parent_component() is block for b in block.values()) # Check __contains__ for i in block_set: assert i in block # Check attributes and subblocks of each data object for i, b in block.items(): assert b.mod is model_map[i] assert b.time is time_map[i] assert all(i1 is i2 for i1, i2 in zip(b._inputs, inputs_map[i])) assert all(i1 is i2 for i1, i2 in zip(b._measurements, measurements_map[i])) assert hasattr(b, 'category_dict') assert hasattr(b, 'vardata_map') assert hasattr(b, 'measurement_vars') assert hasattr(b, 'differential_vars') assert hasattr(b, 'algebraic_vars') assert hasattr(b, 'derivative_vars') assert hasattr(b, 'input_vars') assert hasattr(b, 'fixed_vars') subblocks = [ b.mod, b.vectors, b.DIFFERENTIAL_BLOCK, b.ALGEBRAIC_BLOCK, b.INPUT_BLOCK, b.FIXED_BLOCK, b.DERIVATIVE_BLOCK, b.MEASUREMENT_BLOCK, ] block_objects = ComponentSet( b.component_objects(pyo.Block, descend_into=False)) assert len(subblocks) == len(block_objects) for sb in subblocks: assert sb in block_objects b.v = pyo.Var(initialize=3) b.c = pyo.Constraint(expr=b.v == 5) assert b.v.value == 3 assert b.v in ComponentSet(identify_variables(b.c.expr))
def initialize_by_time_element(fs, time, **kwargs): """ Function to initialize Flowsheet fs element-by-element along ContinuousSet time. Assumes sufficient initialization/correct degrees of freedom such that the first finite element can be solved immediately and each subsequent finite element can be solved by fixing differential and derivative variables at the initial time point of that finite element. Args: fs : Flowsheet to initialize time : Set whose elements will be solved for individually solver : Pyomo solver object initialized with user's desired options outlvl : IDAES logger outlvl ignore_dof : Bool. If True, checks for square problems will be skipped. Returns: None """ if not isinstance(fs, FlowsheetBlock): raise TypeError('First arg must be a FlowsheetBlock') if not isinstance(time, ContinuousSet): raise TypeError('Second arg must be a ContinuousSet') if time.get_discretization_info() == {}: raise ValueError('ContinuousSet must be discretized') scheme = time.get_discretization_info()['scheme'] fep_list = time.get_finite_elements() nfe = time.get_discretization_info()['nfe'] if scheme == 'LAGRANGE-RADAU': ncp = time.get_discretization_info()['ncp'] elif scheme == 'LAGRANGE-LEGENDRE': msg = 'Initialization does not support collocation with Legendre roots' raise NotImplementedError(msg) elif scheme == 'BACKWARD Difference': ncp = 1 elif scheme == 'FORWARD Difference': ncp = 1 msg = 'Forward initialization (explicit Euler) has not yet been implemented' raise NotImplementedError(msg) elif scheme == 'CENTRAL Difference': msg = 'Initialization does not support central finite difference' raise NotImplementedError(msg) else: msg = 'Unrecognized discretization scheme. ' 'Has the model been discretized along the provided ContinuousSet?' raise ValueError(msg) # Disallow Central/Legendre discretizations. # Neither of these seem to be square by default for multi-finite element # initial value problems. # Create logger objects outlvl = kwargs.pop('outlvl', idaeslog.NOTSET) init_log = idaeslog.getInitLogger(__name__, level=outlvl) solver_log = idaeslog.getSolveLogger(__name__, level=outlvl) ignore_dof = kwargs.pop('ignore_dof', False) solver = kwargs.pop('solver', SolverFactory('ipopt')) fix_diff_only = kwargs.pop('fix_diff_only', True) # This option makes the assumption that the only variables that # link constraints to previous points in time (which must be fixed) # are the derivatives and differential variables. Not true if a controller # is being present, but should be a good assumption otherwise, and is # significantly faster than searching each constraint for time-linking # variables. if not ignore_dof: if degrees_of_freedom(fs) != 0: msg = ('Original model has nonzero degrees of freedom. This was ' 'unexpected. Use keyword arg igore_dof=True to skip this ' 'check.') init_log.error(msg) raise ValueError('Nonzero degrees of freedom.') # Get dict telling which constraints/blocks are already inactive: # dict: id(compdata) -> bool (is active?) was_originally_active = get_activity_dict(fs) # Deactivate flowsheet except at t0, solve to ensure consistency # of initial conditions. non_initial_time = [t for t in time] non_initial_time.remove(time.first()) deactivated = deactivate_model_at(fs, time, non_initial_time, outlvl=idaeslog.ERROR) if not ignore_dof: if degrees_of_freedom(fs) != 0: msg = ( 'Model has nonzero degrees of freedom at initial conditions.' ' This was unexpected. Use keyword arg igore_dof=True to skip' ' this check.') init_log.error(msg) raise ValueError('Nonzero degrees of freedom.') init_log.info( 'Model is inactive except at t=0. Solving for consistent initial conditions.' ) with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(fs, tee=slc.tee) if results.solver.termination_condition == TerminationCondition.optimal: init_log.info('Successfully solved for consistent initial conditions') else: init_log.error('Failed to solve for consistent initial conditions') raise ValueError('Solver failed in initialization') deactivated[time.first()] = deactivate_model_at( fs, time, time.first(), outlvl=idaeslog.ERROR)[time.first()] # Here, deactivate non-time-indexed components. Do this after solve # for initial conditions in case these were used to specify initial # conditions con_unindexed_by_time = deactivate_constraints_unindexed_by(fs, time) var_unindexed_by_time = fix_vars_unindexed_by(fs, time) # Now model is completely inactive # For each timestep, we need to # 1. Activate model at points we're solving for # 2. Fix initial conditions (differential variables at previous timestep) # of finite element # 3. Solve the (now) square system # 4. Revert the model to its prior state # This will make use of the following dictionaries mapping # time points -> time derivatives and time-differential variables derivs_at_time = get_derivatives_at(fs, time, [t for t in time]) dvars_at_time = { t: [ d.parent_component().get_state_var()[d.index()] for d in derivs_at_time[t] ] for t in time } # Perform a solve for 1 -> nfe; i is the index of the finite element init_log.info( 'Flowsheet has been deactivated. Beginning element-wise initialization' ) for i in range(1, nfe + 1): t_prev = time[(i - 1) * ncp + 1] # Non-initial time points in the finite element: fe = [time[k] for k in range((i - 1) * ncp + 2, i * ncp + 2)] init_log.info(f'Entering step {i}/{nfe} of initialization') # Activate components of model that were active in the presumably # square original system for t in fe: for comp in deactivated[t]: if was_originally_active[id(comp)]: comp.activate() # Get lists of derivative and differential variables # at initial time point of finite element init_deriv_list = derivs_at_time[t_prev] init_dvar_list = dvars_at_time[t_prev] # Variables that were originally fixed fixed_vars = [] if fix_diff_only: for drv in init_deriv_list: # Cannot fix variables with value None. # Any variable with value None was not solved for # (either stale or not included in previous solve) # and we don't want to fix it. if not drv.fixed: fixed_vars.append(drv) if not drv.value is None: drv.fix() for dv in init_dvar_list: if not dv.fixed: fixed_vars.append(dv) if not dv.value is None: dv.fix() else: for con in fs.component_data_objects(Constraint, active=True): for var in identify_variables(con.expr, include_fixed=False): t_idx = get_implicit_index_of_set(var, time) if t_idx is None: continue if t_idx <= t_prev: fixed_vars.append(var) var.fix() # Initialize finite element from its initial conditions for t in fe: copy_values_at_time(fs, fs, t, t_prev, copy_fixed=False, outlvl=idaeslog.ERROR) # Log that we are solving finite element {i} init_log.info(f'Solving finite element {i}') if not ignore_dof: if degrees_of_freedom(fs) != 0: msg = ( f'Model has nonzero degrees of freedom at finite element' ' {i}. This was unexpected. ' 'Use keyword arg igore_dof=True to skip this check.') init_log.error(msg) raise ValueError('Nonzero degrees of freedom') with idaeslog.solver_log(solver_log, level=idaeslog.DEBUG) as slc: results = solver.solve(fs, tee=slc.tee) if results.solver.termination_condition == TerminationCondition.optimal: init_log.info(f'Successfully solved finite element {i}') else: init_log.error(f'Failed to solve finite element {i}') raise ValueError('Failure in initialization solve') # Deactivate components that may have been activated for t in fe: for comp in deactivated[t]: comp.deactivate() # Unfix variables that have been fixed for var in fixed_vars: var.unfix() # Log that initialization step {i} has been finished init_log.info(f'Initialization step {i} complete') # Reactivate components of the model that were originally active for t in time: for comp in deactivated[t]: if was_originally_active[id(comp)]: comp.activate() for con in con_unindexed_by_time: con.activate() for var in var_unindexed_by_time: var.unfix() # Logger message that initialization is finished init_log.info('Initialization completed. Model has been reactivated')
def fbbt_block(m, tol=1e-4, deactivate_satisfied_constraints=False, integer_tol=1e-5, infeasible_tol=1e-8): """ Feasibility based bounds tightening (FBBT) for a block or model. This loops through all of the constraints in the block and performs FBBT on each constraint (see the docstring for fbbt_con()). Through this processes, any variables whose bounds improve by more than tol are collected, and FBBT is performed again on all constraints involving those variables. This process is continued until no variable bounds are improved by more than tol. Parameters ---------- m: pyomo.core.base.block.Block or pyomo.core.base.PyomoModel.ConcreteModel tol: float deactivate_satisfied_constraints: bool If deactivate_satisfied_constraints is True and a constraint is always satisfied, then the constranit will be deactivated integer_tol: float If the lower bound computed on a binary variable is less than or equal to integer_tol, then the lower bound is left at 0. Otherwise, the lower bound is increased to 1. If the upper bound computed on a binary variable is greater than or equal to 1-integer_tol, then the upper bound is left at 1. Otherwise the upper bound is decreased to 0. infeasible_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than infeasible_tol, then the constraint is considered infeasible and an exception is raised. Returns ------- new_var_bounds: ComponentMap A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ new_var_bounds = ComponentMap() var_to_con_map = ComponentMap() var_lbs = ComponentMap() var_ubs = ComponentMap() for c in m.component_data_objects(ctype=Constraint, active=True, descend_into=True, sort=True): for v in identify_variables(c.body): if v not in var_to_con_map: var_to_con_map[v] = list() if v.lb is None: var_lbs[v] = -math.inf else: var_lbs[v] = value(v.lb) if v.ub is None: var_ubs[v] = math.inf else: var_ubs[v] = value(v.ub) var_to_con_map[v].append(c) for _v in m.component_data_objects(ctype=Var, active=True, descend_into=True, sort=True): if _v.is_fixed(): _v.setlb(_v.value) _v.setub(_v.value) new_var_bounds[_v] = (_v.value, _v.value) improved_vars = ComponentSet() for c in m.component_data_objects(ctype=Constraint, active=True, descend_into=True, sort=True): _new_var_bounds = fbbt_con(c, deactivate_satisfied_constraints=deactivate_satisfied_constraints, integer_tol=integer_tol, infeasible_tol=infeasible_tol) new_var_bounds.update(_new_var_bounds) for v, bnds in _new_var_bounds.items(): vlb, vub = bnds if vlb is not None: if vlb > var_lbs[v] + tol: improved_vars.add(v) var_lbs[v] = vlb if vub is not None: if vub < var_ubs[v] - tol: improved_vars.add(v) var_ubs[v] = vub while len(improved_vars) > 0: v = improved_vars.pop() for c in var_to_con_map[v]: _new_var_bounds = fbbt_con(c, deactivate_satisfied_constraints=deactivate_satisfied_constraints, integer_tol=integer_tol, infeasible_tol=infeasible_tol) new_var_bounds.update(_new_var_bounds) for _v, bnds in _new_var_bounds.items(): _vlb, _vub = bnds if _vlb is not None: if _vlb > var_lbs[_v] + tol: improved_vars.add(_v) var_lbs[_v] = _vlb if _vub is not None: if _vub < var_ubs[_v] - tol: improved_vars.add(_v) var_ubs[_v] = _vub return new_var_bounds
def test_init_rule(self): block_set = pyo.Set(initialize=range(3)) block_set.construct() # Create same maps as before horizon_map = {0: 1, 1: 3, 2: 5} nfe_map = {0: 2, 1: 6, 2: 10} model_map = { i: make_model(horizon=horizon_map[i], nfe=nfe_map[i]) for i in block_set } # Create rule to construct DynamicBlock with def dynamic_block_rule(b, i): model = model_map[i] time = model.time t0 = time.first() inputs = [model.flow_in[t0]] measurements = [model.conc[0, 'A'], model.conc[0, 'B']] # Won't be obvious that these attrs need to be set if # constructing from a rule b.mod = model super(_BlockData, b).__setattr__('time', time) b._inputs = inputs b._measurements = measurements # Create DynamicBlock from a rule block = DynamicBlock(block_set, rule=dynamic_block_rule) assert type(block) is IndexedDynamicBlock assert isinstance(block, DynamicBlock) block.construct() # Make sure iterating over block.values works as expected assert all(b.parent_component() is block for b in block.values()) # Make sure __contains__ works for i in block_set: assert i in block # Assert correct attributes and subblocks for i, b in block.items(): assert b.mod is model_map[i] assert b.time is model_map[i].time t0 = b.time.first() assert all( i1 is i2 for i1, i2 in zip(b._inputs, [model_map[i].flow_in[t0]])) assert all(i1 is i2 for i1, i2 in zip( b._measurements, [model_map[i].conc[t0, 'A'], model_map[i].conc[t0, 'B']])) assert hasattr(b, 'category_dict') assert hasattr(b, 'vardata_map') assert hasattr(b, 'measurement_vars') assert hasattr(b, 'differential_vars') assert hasattr(b, 'algebraic_vars') assert hasattr(b, 'derivative_vars') assert hasattr(b, 'input_vars') assert hasattr(b, 'fixed_vars') subblocks = [ b.mod, b.vectors, b.DIFFERENTIAL_BLOCK, b.ALGEBRAIC_BLOCK, b.INPUT_BLOCK, b.FIXED_BLOCK, b.DERIVATIVE_BLOCK, b.MEASUREMENT_BLOCK, ] block_objects = ComponentSet( b.component_objects(pyo.Block, descend_into=False)) assert len(subblocks) == len(block_objects) for sb in subblocks: assert sb in block_objects b.v = pyo.Var(initialize=3) b.c = pyo.Constraint(expr=b.v == 5) assert b.v.value == 3 assert b.v in ComponentSet(identify_variables(b.c.expr))
def test_init_simple(self): model = make_model(horizon=1, nfe=2) time = model.time t0 = time.first() inputs = [model.flow_in[t0]] measurements = [model.conc[0, 'A'], model.conc[0, 'B']] block = DynamicBlock( model=model, time=time, inputs=inputs, measurements=measurements, ) # Assert that we have the correct type assert type(block) is SimpleDynamicBlock assert isinstance(block, DynamicBlock) assert isinstance(block, _DynamicBlockData) block.construct() # Assert that we behave like a simple block assert block[None] is block assert all(b is block for b in block[:]) # Assert that input attributes have been processed correctly assert block.mod is model assert block.time is time assert all(i1 is i2 for i1, i2 in zip(block._inputs, inputs)) assert all(i1 is i2 for i1, i2 in zip(block._measurements, measurements)) # Assert that utility attributes have been added assert hasattr(block, 'category_dict') assert hasattr(block, 'vardata_map') assert hasattr(block, 'measurement_vars') assert hasattr(block, 'differential_vars') assert hasattr(block, 'algebraic_vars') assert hasattr(block, 'derivative_vars') assert hasattr(block, 'input_vars') assert hasattr(block, 'fixed_vars') subblocks = [ block.mod, block.vectors, block.DIFFERENTIAL_BLOCK, block.ALGEBRAIC_BLOCK, block.INPUT_BLOCK, block.FIXED_BLOCK, block.DERIVATIVE_BLOCK, block.MEASUREMENT_BLOCK, ] block_objects = ComponentSet( block.component_objects(pyo.Block, descend_into=False)) # Assert that subblocks have been added assert len(subblocks) == len(block_objects) for b in subblocks: assert b in block_objects # Assert that we can add variables and constraints to the block block.v = pyo.Var(initialize=3) block.c = pyo.Constraint(expr=block.v == 5) assert block.v.value == 3 assert block.v in ComponentSet(identify_variables(block.c.expr))
def create_linear_dual_from(block, fixed=None, unfixed=None): """ Construct a block that represents the dual of the given block. The resulting block contains variables and constraints whose names are the dual names of the primal block. Note that this involves a many string operations. A quicker operations could be executed, but it would generate a dual representation that is difficult to interpret. Note that the dualization of a maximization problem is performed by negating objective and right-hand side coefficients after dualizing the corresponding minimization problem. This suggestion is made by Dimitri Bertsimas and John Tsitsiklis in section 4.2 page 143 of "Introduction to Linear Optimization" Arguments: block: A Pyomo block or model unfixed: An iterable object with Variable and VarData values that are not fixed variables. All other variables are assumed to be fixed. fixed: An iterable object with Variable and VarData values that are fixed. All other variables are assumed not fixed. Returns: If the block is a model object, then this returns a ConcreteModel. Otherwise, it returns a Block. """ # # Collect vardata that needs to be fixed # fixed_modelvars = {} if fixed or unfixed: # # Collect model variables # modelvars = {} # # vardata in objectives # for obj in block.component_objects(Objective, active=True): for ndx in obj: #odata = generate_standard_repn(obj[ndx].expr, compute_values=False) for vdata in identify_variables(obj[ndx].expr, include_fixed=False): id_ = id(vdata) if not id_ in modelvars: modelvars[id_] = vdata # # vardata in constraints # for con in block.component_objects(Constraint, active=True): for ndx in con: #cdata = generate_standard_repn(con[ndx].body, compute_values=False) for vdata in identify_variables(con[ndx].body, include_fixed=False): id_ = id(vdata) if not id_ in modelvars: modelvars[id_] = vdata # # Fix everything that isn't specified as unfixed # if unfixed: unfixed_vars = set() for v in unfixed: if v.is_indexed(): for vardata in v.values(): unfixed_vars.add(id(vardata)) else: unfixed_vars.add(id(v)) for id_, vdata in modelvars.items(): if id_ not in unfixed_vars: fixed_modelvars[id_] = vdata # # ... or fix everything that is specified as fixed # elif fixed: fixed_vars = set() for v in fixed: if v.is_indexed(): for vardata in v.values(): fixed_vars.add(id(vardata)) else: fixed_vars.add(id(v)) for id_ in fixed_vars: if id_ in modelvars: fixed_modelvars[id_] = modelvars[id_] A, b_coef, obj_constant, c_rhs, c_sense, d_sense, v_domain =\ collect_dual_representation(block, fixed_modelvars) # # Construct the block # if isinstance(block, Model): dual = ConcreteModel() else: dual = Block() dual.construct() _vars = {} # Return variable object from name and index (if applicable) def getvar(name, ndx=None): v = _vars.get((name, ndx), None) if v is None: v = Var() if ndx is None: v_name = name elif isinstance(ndx, tuple): v_name = "%s[%s]" % (name, ','.join(map(str, ndx))) else: v_name = "%s[%s]" % (name, str(ndx)) setattr(dual, v_name, v) _vars[name, ndx] = v return v # # Construct the objective # The dualization of a maximization problem is handled by simply negating the # objective and left-hand side coefficients while keeping the dual sense. # if d_sense == minimize: dual.o = Objective(expr=obj_constant + sum(-b_coef[name, ndx] * getvar(name, ndx) for name, ndx in b_coef), sense=d_sense) rhs_multiplier = -1 else: dual.o = Objective(expr=obj_constant + sum(b_coef[name, ndx] * getvar(name, ndx) for name, ndx in b_coef), sense=d_sense) rhs_multiplier = 1 # # Construct the constraints from dual A matrix # for cname in A: for ndx, terms in A[cname].items(): # Build left-hand side of constraint expr = 0 for term in terms: expr += term.coef * getvar(term.var, term.ndx) # # Assign right-hand side coefficient # Note that rhs_multiplier is 1 if the dual is a maximization problem and -1 otherwise # rhsval = rhs_multiplier * c_rhs.get((cname, ndx), 0.0) # Using the correct inequality or equality if c_sense[cname, ndx] == 'e': e = expr - rhsval == 0 elif c_sense[cname, ndx] == 'l': e = expr - rhsval <= 0 else: e = expr - rhsval >= 0 c = Constraint(expr=e) # Build constraint name if ndx is None: c_name = cname elif isinstance(ndx, tuple): c_name = "%s[%s]" % (cname, ','.join(map(str, ndx))) else: c_name = "%s[%s]" % (cname, str(ndx)) # Add new constraint along with its name to the dual setattr(dual, c_name, c) # Set variable domains for (name, ndx), domain in v_domain.items(): v = getvar(name, ndx) #flag = type(ndx) is tuple and (ndx[-1] == 'lb' or ndx[-1] == 'ub') if domain == 1: v.domain = NonNegativeReals elif domain == -1: v.domain = NonPositiveReals else: # This is possible when the variable's corresponding constraint is an equality v.domain = Reals return dual
def test_identify_vars_vars(self): m = ConcreteModel() m.I = RangeSet(3) m.a = Var(initialize=1) m.b = Var(m.I, initialize=1) m.p = Param(initialize=1, mutable=True) m.x = ExternalFunction(library='foo.so', function='bar') # # Identify variables in various algebraic expressions # self.assertEqual( list(identify_variables(m.a)), [m.a] ) self.assertEqual( list(identify_variables(m.b[1])), [m.b[1]] ) self.assertEqual( list(identify_variables(m.a+m.b[1])), [ m.a, m.b[1] ] ) self.assertEqual( list(identify_variables(m.a**m.b[1])), [ m.a, m.b[1] ] ) self.assertEqual( list(identify_variables(m.a**m.b[1] + m.b[2])), [ m.a, m.b[1], m.b[2] ] ) self.assertEqual( list(identify_variables( m.a**m.b[1] + m.b[2]*m.b[3]*m.b[2])), [ m.a, m.b[1], m.b[2], m.b[3] ] ) self.assertEqual( list(identify_variables( m.a**m.b[1] + m.b[2]/m.b[3]*m.b[2])), [ m.a, m.b[1], m.b[2], m.b[3] ] ) # # Identify variables in the arguments to functions # self.assertEqual( list(identify_variables( m.x(m.a, 'string_param', 1, [])*m.b[1] )), [ m.a, m.b[1] ] ) self.assertEqual( list(identify_variables( m.x(m.p, 'string_param', 1, [])*m.b[1] )), [ m.b[1] ] ) self.assertEqual( list(identify_variables( tanh(m.a)*m.b[1] )), [ m.a, m.b[1] ] ) self.assertEqual( list(identify_variables( abs(m.a)*m.b[1] )), [ m.a, m.b[1] ] ) # # Check logic for allowing duplicates # self.assertEqual( list(identify_variables(m.a**m.a + m.a)), [ m.a ] )
def add_affine_cuts(nlp_result, solve_data, config): with time_code(solve_data.timing, "affine cut generation"): m = solve_data.linear_GDP if config.calc_disjunctive_bounds: with time_code(solve_data.timing, "disjunctive variable bounding"): TransformationFactory('contrib.compute_disj_var_bounds').\ apply_to( m, solver=config.mip_solver if config.obbt_disjunctive_bounds else None ) config.logger.info("Adding affine cuts.") GDPopt = m.GDPopt_utils counter = 0 for var, val in zip(GDPopt.variable_list, nlp_result.var_values): if val is not None and not var.fixed: var.set_value(val, skip_validation=True) for constr in constraints_in_True_disjuncts(m, config): # Note: this includes constraints that are deactivated in the # current model (linear_GDP) disjunctive_var_bounds = disjunctive_bounds(constr.parent_block()) if constr.body.polynomial_degree() in (1, 0): continue vars_in_constr = list(identify_variables(constr.body)) if any(var.value is None for var in vars_in_constr): continue # a variable has no values # mcpp stuff try: mc_eqn = mc(constr.body, disjunctive_var_bounds) except MCPP_Error as e: config.logger.debug("Skipping constraint %s due to MCPP " "error %s" % (constr.name, str(e))) continue # skip to the next constraint ccSlope = mc_eqn.subcc() cvSlope = mc_eqn.subcv() ccStart = mc_eqn.concave() cvStart = mc_eqn.convex() ub_int = min(constr.upper, mc_eqn.upper()) if constr.has_ub() \ else mc_eqn.upper() lb_int = max(constr.lower, mc_eqn.lower()) if constr.has_lb() \ else mc_eqn.lower() parent_block = constr.parent_block() # Create a block on which to put outer approximation cuts. aff_utils = parent_block.component('GDPopt_aff') if aff_utils is None: aff_utils = parent_block.GDPopt_aff = Block( doc="Block holding affine constraints") aff_utils.GDPopt_aff_cons = ConstraintList() aff_cuts = aff_utils.GDPopt_aff_cons concave_cut = sum(ccSlope[var] * (var - var.value) for var in vars_in_constr if not var.fixed) + ccStart >= lb_int convex_cut = sum(cvSlope[var] * (var - var.value) for var in vars_in_constr if not var.fixed) + cvStart <= ub_int aff_cuts.add(expr=concave_cut) aff_cuts.add(expr=convex_cut) counter += 2 config.logger.info("Added %s affine cuts" % counter)
def log_infeasible_constraints(m, tol=1E-6, logger=logger, log_expression=False, log_variables=False): """Print the infeasible constraints in the model. Uses the current model state. Uses pyomo.util.infeasible logger unless one is provided. Args: m (Block): Pyomo block or model to check tol (float): feasibility tolerance log_expression (bool): If true, prints the constraint expression log_variables (bool): If true, prints the constraint variable names and values """ # Iterate through all active constraints on the model for constr in m.component_data_objects(ctype=Constraint, active=True, descend_into=True): constr_body_value = value(constr.body, exception=False) constr_lb_value = value(constr.lower, exception=False) constr_ub_value = value(constr.upper, exception=False) constr_undefined = False equality_violated = False lb_violated = False ub_violated = False if constr_body_value is None: # Undefined constraint body value due to missing variable value constr_undefined = True pass else: # Check for infeasibilities if constr.equality: if fabs(constr_lb_value - constr_body_value) >= tol: equality_violated = True else: if constr.has_lb() and fabs(constr_lb_value - constr_body_value) >= tol: lb_violated = True if constr.has_ub() and fabs(constr_body_value - constr_ub_value) >= tol: ub_violated = True if not any( (constr_undefined, equality_violated, lb_violated, ub_violated)): # constraint is fine. skip to next constraint continue output_dict = dict(name=constr.name) log_template = "CONSTR {name}: {lb_value}{lb_operator}{body_value}{ub_operator}{ub_value}" if log_expression: log_template += "\n {lb_expr}{lb_operator}{body_expr}{ub_operator}{ub_expr}" if log_variables: vars_template = " VAR {name}: {value}" log_template += "\n{var_printout}" constraint_vars = identify_variables(constr.body, include_fixed=True) output_dict['var_printout'] = '\n'.join( vars_template.format(name=v.name, value=v.value) for v in constraint_vars) output_dict[ 'body_value'] = "missing variable value" if constr_undefined else constr_body_value output_dict['body_expr'] = constr.body if constr.equality: output_dict['lb_value'] = output_dict['lb_expr'] = output_dict[ 'lb_operator'] = "" output_dict['ub_value'] = constr_ub_value output_dict['ub_expr'] = constr.upper if equality_violated: output_dict['ub_operator'] = " =/= " elif constr_undefined: output_dict['ub_operator'] = " =?= " else: if constr.has_lb(): output_dict['lb_value'] = constr_lb_value output_dict['lb_expr'] = constr.lower if lb_violated: output_dict['lb_operator'] = " </= " elif constr_undefined: output_dict['lb_operator'] = " <?= " else: output_dict['lb_operator'] = " <= " else: output_dict['lb_value'] = output_dict['lb_expr'] = output_dict[ 'lb_operator'] = "" if constr.has_ub(): output_dict['ub_value'] = constr_ub_value output_dict['ub_expr'] = constr.upper if ub_violated: output_dict['ub_operator'] = " </= " elif constr_undefined: output_dict['ub_operator'] = " <?= " else: output_dict['ub_operator'] = " <= " else: output_dict['ub_value'] = output_dict['ub_expr'] = output_dict[ 'ub_operator'] = "" logger.info(log_template.format(**output_dict))
def add_outer_approximation_cuts(nlp_result, solve_data, config): """Add outer approximation cuts to the linear GDP model.""" with time_code(solve_data.timing, 'OA cut generation'): m = solve_data.linear_GDP GDPopt = m.GDPopt_utils sign_adjust = -1 if solve_data.objective_sense == minimize else 1 # copy values over for var, val in zip(GDPopt.variable_list, nlp_result.var_values): if val is not None and not var.fixed: var.value = val # TODO some kind of special handling if the dual is phenomenally small? config.logger.debug('Adding OA cuts.') counter = 0 if not hasattr(GDPopt, 'jacobians'): GDPopt.jacobians = ComponentMap() for constr, dual_value in zip(GDPopt.constraint_list, nlp_result.dual_values): if dual_value is None or constr.body.polynomial_degree() in (1, 0): continue # Determine if the user pre-specified that OA cuts should not be # generated for the given constraint. parent_block = constr.parent_block() ignore_set = getattr(parent_block, 'GDPopt_ignore_OA', None) config.logger.debug('Ignore_set %s' % ignore_set) if (ignore_set and (constr in ignore_set or constr.parent_component() in ignore_set)): config.logger.debug( 'OA cut addition for %s skipped because it is in ' 'the ignore set.' % constr.name) continue config.logger.debug("Adding OA cut for %s with dual value %s" % (constr.name, dual_value)) # Cache jacobian jacobian = GDPopt.jacobians.get(constr, None) if jacobian is None: constr_vars = list( identify_variables(constr.body, include_fixed=False)) if len(constr_vars) >= MAX_SYMBOLIC_DERIV_SIZE: mode = differentiate.Modes.reverse_numeric else: mode = differentiate.Modes.sympy try: jac_list = differentiate(constr.body, wrt_list=constr_vars, mode=mode) jac_map = ComponentMap(zip(constr_vars, jac_list)) except: if mode is differentiate.Modes.reverse_numeric: raise mode = differentiate.Modes.reverse_numeric jac_map = ComponentMap() jacobian = JacInfo(mode=mode, vars=constr_vars, jac=jac_map) GDPopt.jacobians[constr] = jacobian # Recompute numeric derivatives if not jacobian.jac: jac_list = differentiate(constr.body, wrt_list=jacobian.vars, mode=jacobian.mode) jacobian.jac.update(zip(jacobian.vars, jac_list)) # Create a block on which to put outer approximation cuts. oa_utils = parent_block.component('GDPopt_OA') if oa_utils is None: oa_utils = parent_block.GDPopt_OA = Block( doc="Block holding outer approximation cuts " "and associated data.") oa_utils.GDPopt_OA_cuts = ConstraintList() oa_utils.GDPopt_OA_slacks = VarList(bounds=(0, config.max_slack), domain=NonNegativeReals, initialize=0) oa_cuts = oa_utils.GDPopt_OA_cuts slack_var = oa_utils.GDPopt_OA_slacks.add() rhs = value(constr.lower) if constr.has_lb() else value( constr.upper) try: new_oa_cut = (copysign(1, sign_adjust * dual_value) * (value(constr.body) - rhs + sum( value(jac) * (var - value(var)) for var, jac in iteritems(jacobian.jac))) - slack_var <= 0) if new_oa_cut.polynomial_degree() not in (1, 0): for var, jac in iteritems(jacobian.jac): print(var.name, value(jac)) oa_cuts.add(expr=new_oa_cut) counter += 1 except ZeroDivisionError: config.logger.warning( "Zero division occured attempting to generate OA cut for constraint %s.\n" "Skipping OA cut generation for this constraint." % (constr.name, )) # Simply continue on to the next constraint. # Clear out the numeric Jacobian values if jacobian.mode is differentiate.Modes.reverse_numeric: jacobian.jac.clear() config.logger.info('Added %s OA cuts' % counter)
def test_identify_vars_linear_expression(self): m = ConcreteModel() m.x = Var() expr = quicksum([m.x, m.x], linear=True) self.assertEqual(list(identify_variables( expr, include_fixed=False)), [m.x])
def add_outer_approximation_cuts(nlp_result, solve_data, config): """Add outer approximation cuts to the linear GDP model.""" with time_code(solve_data.timing, 'OA cut generation'): m = solve_data.linear_GDP GDPopt = m.GDPopt_utils sign_adjust = -1 if solve_data.objective_sense == minimize else 1 # copy values over for var, val in zip(GDPopt.variable_list, nlp_result.var_values): if val is not None and not var.fixed: var.value = val # TODO some kind of special handling if the dual is phenomenally small? config.logger.debug('Adding OA cuts.') counter = 0 if not hasattr(GDPopt, 'jacobians'): GDPopt.jacobians = ComponentMap() for constr, dual_value in zip(GDPopt.constraint_list, nlp_result.dual_values): if dual_value is None or constr.body.polynomial_degree() in (1, 0): continue # Determine if the user pre-specified that OA cuts should not be # generated for the given constraint. parent_block = constr.parent_block() ignore_set = getattr(parent_block, 'GDPopt_ignore_OA', None) config.logger.debug('Ignore_set %s' % ignore_set) if (ignore_set and (constr in ignore_set or constr.parent_component() in ignore_set)): config.logger.debug( 'OA cut addition for %s skipped because it is in ' 'the ignore set.' % constr.name) continue config.logger.debug( "Adding OA cut for %s with dual value %s" % (constr.name, dual_value)) # Cache jacobians jacobians = GDPopt.jacobians.get(constr, None) if jacobians is None: constr_vars = list(identify_variables(constr.body)) jac_list = differentiate(constr.body, wrt_list=constr_vars) jacobians = ComponentMap(zip(constr_vars, jac_list)) GDPopt.jacobians[constr] = jacobians # Create a block on which to put outer approximation cuts. oa_utils = parent_block.component('GDPopt_OA') if oa_utils is None: oa_utils = parent_block.GDPopt_OA = Block( doc="Block holding outer approximation cuts " "and associated data.") oa_utils.GDPopt_OA_cuts = ConstraintList() oa_utils.GDPopt_OA_slacks = VarList( bounds=(0, config.max_slack), domain=NonNegativeReals, initialize=0) oa_cuts = oa_utils.GDPopt_OA_cuts slack_var = oa_utils.GDPopt_OA_slacks.add() rhs = value(constr.lower) if constr.has_lb() else value(constr.upper) oa_cuts.add( expr=copysign(1, sign_adjust * dual_value) * ( value(constr.body) - rhs + sum( value(jacobians[var]) * (var - value(var)) for var in jacobians)) - slack_var <= 0) counter += 1 config.logger.info('Added %s OA cuts' % counter)
def test_cubic(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) rel = coramin.relaxations.relax(m) # this problem should turn into # # aux2 + y + z = 0 => aux_con[1] # w - 3*aux2 = 0 => aux_con[2] # aux1 = x**2 => rel0 # aux2 = x*aux1 => rel1 self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) self.assertEqual(len(rel.aux_cons), 2) self.assertEqual(len(rel.aux_vars), 2) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) self.assertAlmostEqual(rel.aux_vars[2].lb, -1) self.assertAlmostEqual(rel.aux_vars[2].ub, 1) self.assertEqual(rel.aux_cons[1].lower, 0) self.assertEqual(rel.aux_cons[1].upper, 0) ders = reverse_sd(rel.aux_cons[1].body) self.assertEqual(ders[rel.z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) self.assertEqual(ders[rel.y], 1) self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) self.assertEqual(rel.aux_cons[2].lower, 0) self.assertEqual(rel.aux_cons[2].upper, 0) ders = reverse_sd(rel.aux_cons[2].body) self.assertEqual(ders[rel.w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) self.assertTrue( isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel1.get_rhs_vars())) self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var()))
def test_pow_neg(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-2) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m) # This model should be relaxed to # # aux2 + y + z = 0 # w - 3 * aux2 = 0 # aux1 = x**2 # aux1*aux2 = aux3 # aux3 = 1 # self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) self.assertEqual(len(rel.aux_cons), 2) self.assertEqual(len(rel.aux_vars), 3) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) self.assertTrue(rel.aux_vars[3].is_fixed()) self.assertEqual(rel.aux_vars[3].value, 1) self.assertEqual(rel.aux_cons[1].lower, 0) self.assertEqual(rel.aux_cons[1].upper, 0) ders = reverse_sd(rel.aux_cons[1].body) self.assertEqual(ders[rel.z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) self.assertEqual(ders[rel.y], 1) self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) self.assertEqual(rel.aux_cons[2].lower, 0) self.assertEqual(rel.aux_cons[2].upper, 0) ders = reverse_sd(rel.aux_cons[2].body) self.assertEqual(ders[rel.w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) self.assertTrue(hasattr(rel.relaxations, 'rel1')) self.assertTrue( isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) self.assertIn(rel.aux_vars[2], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[3]), id(rel.relaxations.rel1.get_aux_var())) self.assertFalse(hasattr(rel.relaxations, 'rel2'))
def replaceExternalFunctionsWithVariables(self): """ This method sets up essential data objects on the new trf_data block on the model as well as triggers the replacement of external functions in expressions trees. Data objects created: self.data.all_variables : ComponentSet A set of all variables on the model, including "holder" variables from the EF replacement self.data.truth_models : ComponentMap A component map for replaced nodes that keeps track of the truth model for that replacement. self.data.basis_expressions : ComponentMap A component map for the Pyomo expressions for basis functions as they apply to each variable self.data.ef_inputs : Dict A dictionary that tracks the input variables for each EF self.data.ef_outputs : VarList A list of the "holder" variables which replaced the original External Function expressions """ self.data.all_variables = ComponentSet() self.data.truth_models = ComponentMap() self.data.basis_expressions = ComponentMap() self.data.ef_inputs = {} self.data.ef_outputs = VarList() number_of_equality_constraints = 0 for con in self.model.component_data_objects(Constraint, active=True): if con.lb == con.ub and con.lb is not None: number_of_equality_constraints += 1 self._remove_ef_from_expr(con) self.degrees_of_freedom = (len(list(self.data.all_variables)) - number_of_equality_constraints) if self.degrees_of_freedom != len(self.decision_variables): raise ValueError( "replaceExternalFunctionsWithVariables: " "The degrees of freedom %d do not match the number of decision " "variables supplied %d." % (self.degrees_of_freedom, len(self.decision_variables))) for var in self.decision_variables: if var not in self.data.all_variables: raise ValueError( "replaceExternalFunctionsWithVariables: " f"The supplied decision variable {var.name} cannot " "be found in the model variables.") self.data.objs = list( self.model.component_data_objects(Objective, active=True)) # HACK: This is a hack that we will want to remove once the NL writer # has been corrected to not send unused EFs to the solver for ef in self.model.component_objects(ExternalFunction): ef.parent_block().del_component(ef) if len(self.data.objs) != 1: raise ValueError( "replaceExternalFunctionsWithVariables: " "TrustRegion only supports models with a single active Objective." ) if self.data.objs[0].sense == maximize: self.data.objs[0].expr = -1 * self.data.objs[0].expr self.data.objs[0].sense = minimize self._remove_ef_from_expr(self.data.objs[0]) for i in self.data.ef_outputs: self.data.ef_inputs[i] = \ list(identify_variables( self.data.truth_models[self.data.ef_outputs[i]], include_fixed=False) ) self.data.all_variables.update(self.data.ef_outputs.values()) self.data.all_variables = list(self.data.all_variables)